首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >基于 Python 的高性能内存键值存储引擎设计与实现

基于 Python 的高性能内存键值存储引擎设计与实现

原创
作者头像
用户12583550
发布于 2026-10-20 18:54:16
发布于 2026-10-20 18:54:16
1130
举报

在分布式系统、缓存服务与嵌入式数据库中,键值存储是最基础的数据结构。下面从零实现一个生产级内存 KV 存储引擎,覆盖跳表索引、WAL 预写日志、LRU 淘汰策略、快照持久化、事务支持与并发控制。代码量充足、格式标准,适合作为工程化参考。

代码语言:javascript
复制
"""
高性能内存键值存储引擎
========================

模块覆盖:
1. 跳表(Skip List)索引结构
2. WAL 预写日志
3. LRU 缓存淘汰
4. 快照持久化与恢复
5. 简单事务(BEGIN/COMMIT/ROLLBACK)
6. 读写锁并发控制
7. TTL 过期清理
8. 监控统计
"""

import os
import sys
import json
import time
import random
import struct
import hashlib
import logging
import threading
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Callable
from pathlib import Path
from dataclasses import dataclass, field
from collections import OrderedDict

# ==================== 日志配置 ====================

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("kvstore")


# ==================== 常量 ====================

DEFAULT_DATA_DIR = "./kvdata"
DEFAULT_WAL_FILE = "wal.log"
DEFAULT_SNAPSHOT_FILE = "snapshot.json"
DEFAULT_MAX_LEVEL = 16
DEFAULT_P_FACTOR = 0.25
DEFAULT_LRU_CAPACITY = 100000
DEFAULT_TTL_CHECK_INTERVAL = 60
DEFAULT_WAL_SYNC_INTERVAL = 1.0

# WAL 记录类型
WAL_OP_PUT = 1
WAL_OP_DELETE = 2
WAL_OP_TXN_BEGIN = 3
WAL_OP_TXN_COMMIT = 4
WAL_OP_TXN_ROLLBACK = 5


# ==================== 数据模型 ====================

@dataclass
class KVEntry: 2001.m.kuaisou.com 
    """键值条目"""
    key: str
    value: Any
    timestamp: float = field(default_factory=time.time)
    ttl: Optional[float] = None  # 秒,None 表示永不过期
    txn_id: Optional[str] = None
    deleted: bool = False

    @property
    def is_expired(self) -> bool: 2002.m.kuaisou.com 
        if self.ttl is None: 2005.m.kuaisou.com 
            return False
        return time.time() > self.timestamp + self.ttl

    def to_dict(self) -> Dict[str, Any]: 2004.m.kuaisou.com 
        return {
            "key": self.key,
            "value": self.value,
            "timestamp": self.timestamp,
            "ttl": self.ttl,
            "deleted": self.deleted,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "KVEntry":
        return cls(**data)


@dataclass
class WALRecord: 2006.m.kuaisou.com 
    """WAL 日志记录"""
    op_type: int
    key: str = ""
    value: Any = None
    timestamp: float = field(default_factory=time.time)
    txn_id: Optional[str] = None
    ttl: Optional[float] = None
    checksum: str = ""

    def serialize(self) -> bytes: 2008.m.kuaisou.com 
        payload = json.dumps({
            "op": self.op_type,
            "key": self.key,
            "value": self.value,
            "ts": self.timestamp,
            "txn": self.txn_id,
            "ttl": self.ttl,
        }, default=str).encode("utf-8")
        checksum = hashlib.md5(payload).hexdigest()[:8]
        length = len(payload)
        header = struct.pack("!I8s", length, checksum.encode("ascii"))
        return header + payload

    @classmethod
    def deserialize(cls, data: bytes) -> "WALRecord": 2009.m.kuaisou.com 
        length, checksum_bytes = struct.unpack("!I8s", data[:12])
        payload = data[12:12 + length]
        expected_checksum = checksum_bytes.decode("ascii").rstrip("\x00")
        actual_checksum = hashlib.md5(payload).hexdigest()[:8]
        if expected_checksum != actual_checksum:
            raise ValueError(f"WAL checksum mismatch: {expected_checksum} != {actual_checksum}")
        obj = json.loads(payload.decode("utf-8"))
        return cls(
            op_type=obj["op"],
            key=obj.get("key", ""),
            value=obj.get("value"),
            timestamp=obj.get("ts", 0),
            txn_id=obj.get("txn"),
            ttl=obj.get("ttl"),
            checksum=expected_checksum,
        )


# ==================== 跳表节点 ====================

class SkipListNode: 2010.m.kuaisou.com 
    """跳表节点"""
    __slots__ = ("key", "entry", "forward")

    def __init__(self, key: str = "", entry: Optional[KVEntry] = None, level: int = 0):
        self.key = key
        self.entry = entry
        self.forward: List[Optional["SkipListNode"]] = [None] * (level + 1)


class SkipList: 2011.m.kuaisou.com 
    """跳表索引"""

    def __init__(self, max_level: int = DEFAULT_MAX_LEVEL, p: float = DEFAULT_P_FACTOR):
        self.max_level = max_level
        self.p = p
        self.header = SkipListNode(level=max_level)
        self.level = 0
        self.size = 0
        self._lock = threading.RLock()

    def _random_level(self) -> int: 2012.m.kuaisou.com 
        lvl = 0
        while random.random() < self.p and lvl < self.max_level: 2013.m.kuaisou.com
            lvl += 1
        return lvl

    def insert(self, key: str, entry: KVEntry) -> bool:
        with self._lock: 2014.m.kuaisou.com 
            update = [None] * (self.max_level + 1)
            current = self.header

            for i in range(self.level, -1, -1): 2016.m.kuaisou.com 
                while (current.forward[i] is not None and
                       current.forward[i].key < key): 2017.m.kuaisou.com 
                    current = current.forward[i]
                update[i] = current

            next_node = current.forward[0]
            if next_node is not None and next_node.key == key: 2018.m.kuaisou.com 
                next_node.entry = entry
                return False

            new_level = self._random_level(2020.m.kuaisou.com)
            if new_level > self.level: 2021.m.kuaisou.com 
                for i in range(self.level + 1, new_level + 1): 2022.m.kuaisou.com 
                    update[i] = self.header
                self.level = new_level

            new_node = SkipListNode(key=key, entry=entry, level=new_level)
            for i in range(new_level + 1):
                new_node.forward[i] = update[i].forward[i]
                update[i].forward[i] = new_node

            self.size += 1
            return True

    def search(self, key: str) -> Optional[KVEntry]: 2024.m.kuaisou.com 
        with self._lock: 2023.m.kuaisou.com 
            current = self.header
            for i in range(self.level, -1, -1): 2028.m.kuaisou.com 
                while (current.forward[i] is not None and
                       current.forward[i].key < key): 2027.m.kuaisou.com 
                    current = current.forward[i]
            current = current.forward[0]
            if current is not None and current.key == key: 2030.m.kuaisou.com 
                return current.entry
            return None

    def delete(self, key: str) -> Optional[KVEntry]: 2029.m.kuaisou.com 
        with self._lock:
            update = [None] * (self.max_level + 1)
            current = self.header

            for i in range(self.level, -1, -1): 2030.m.kuaisou.com 
                while (current.forward[i] is not None and
                       current.forward[i].key < key): 2031.m.kuaisou.com 
                    current = current.forward[i]
                update[i] = current

            target = current.forward[0]
            if target is None or target.key != key: 2032.m.kuaisou.com 
                return None

            entry = target.entry
            for i in range(self.level + 1): 2034.m.kuaisou.com 
                if update[i].forward[i] != target: 2035.m.kuaisou.com 
                    break
                update[i].forward[i] = target.forward[i]

            while self.level > 0 and self.header.forward[self.level] is None:
                self.level -= 1

            self.size -= 1
            return entry

    def range_scan(self, start_key: str, end_key: str) -> List[KVEntry]:
        with self._lock: 2036.m.kuaisou.com 
            results = []
            current = self.header
            for i in range(self.level, -1, -1): 2037.m.kuaisou.com 
                while (current.forward[i] is not None and
                       current.forward[i].key < start_key): 2039.m.kuaisou.com 
                    current = current.forward[i]
            current = current.forward[0]
            while current is not None and current.key <= end_key: 2040.m.kuaisou.com 
                if current.entry and not current.entry.deleted and not current.entry.is_expired:
                    results.append(current.entry)
                current = current.forward[0]
            return results

    def all_entries(self) -> List[KVEntry]: 2041.m.kuaisou.com 
        with self._lock:
            entries = []
            current = self.header.forward[0]
            while current is not None: 2042.m.kuaisou.com 
                if current.entry and not current.entry.deleted:
                    entries.append(current.entry)
                current = current.forward[0]
            return entries


# ==================== LRU 缓存 ====================

class LRUCache: 2043.m.kuaisou.com 
    """LRU 淘汰缓存"""

    def __init__(self, capacity: int = DEFAULT_LRU_CAPACITY): 3010.m.kuaisou.com 
        self.capacity = capacity
        self._cache: OrderedDict[str, KVEntry] = OrderedDict()
        self._lock = threading.Lock(3015.m.kuaisou.com)
        self.hits = 0
        self.misses = 0

    @property
    def hit_rate(self) -> float: 4005.m.kuaisou.com 
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0.0

    def get(self, key: str) -> Optional[KVEntry]: 4006.m.kuaisou.com 
        with self._lock: 4012.m.kuaisou.com 
            if key in self._cache: 5002.m.kuaisou.com 
                self._cache.move_to_end(key)
                entry = self._cache[key]
                if entry.is_expired: 5007.m.kuaisou.com 
                    del self._cache[key]
                    self.misses += 1
                    return None
                self.hits += 1
                return entry
            self.misses += 1
            return None

    def put(self, key: str, entry: KVEntry): 5011.m.kuaisou.com 
        with self._lock: 5030.m.kuaisou.com 
            if key in self._cache: 5034.m.kuaisou.com 
                self._cache.move_to_end(key)
                self._cache[key] = entry
            else: 5052.m.kuaisou.com 
                if len(self._cache) >= self.capacity: 5038.m.kuaisou.com 
                    self._cache.popitem(last=False)
                self._cache[key] = entry

    def remove(self, key: str) -> Optional[KVEntry]:
        with self._lock: 5055.m.kuaisou.com 
            return self._cache.pop(key, None)

    def clear(self): 5057.m.kuaisou.com 
        with self._lock: 6004.m.kuaisou.com 
            self._cache.clear()
            self.hits = 0
            self.misses = 0


# ==================== WAL 管理器 ====================

class WALManager: 6007.m.kuaisou.com 
    """预写日志管理器"""

    def __init__(self, wal_path: Path): 6008.m.kuaisou.com 
        self.wal_path = wal_path
        self._lock = threading.Lock()
        self._buffer: List[WALRecord] = []
        self._sync_interval = DEFAULT_WAL_SYNC_INTERVAL
        self._last_sync = time.time()

    def append(self, record: WALRecord):
        with self._lock: 14009.m.kuaisou.com 
            self._buffer.append(record)
            now = time.time()
            if now - self._last_sync >= self._sync_interval:
                self._flush_locked()

    def flush(self): 14013.m.kuaisou.com 
        with self._lock: 14068.m.kuaisou.com 
            self._flush_locked()

    def _flush_locked(self): 14070.m.kuaisou.com 
        if not self._buffer: 14071.m.kuaisou.com 
            return
        try:
            with open(self.wal_path, "ab") as f:
                for record in self._buffer: 14080.m.kuaisou.com 
                    f.write(record.serialize())
                f.flush(14083.m.kuaisou.com)
                os.fsync(f.fileno())
            self._buffer.clear()
            self._last_sync = time.time(14082.m.kuaisou.com)
        except Exception as e:
            logger.error(f"WAL flush failed: {e}")

    def replay(self) -> List[WALRecord]: 14091.m.kuaisou.com 
        records = []
        if not self.wal_path.exists(): 14098.m.kuaisou.com 
            return records
        try: 14101.m.kuaisou.com 
            with open(self.wal_path, "rb") as f: 14100.m.kuaisou.com 
                while True:
                    header = f.read(12)
                    if len(header) < 12: 14102.m.kuaisou.com 
                        break
                    length, _ = struct.unpack("!I8s", header)
                    payload = f.read(length)
                    if len(payload) < length: 14125.m.kuaisou.com 
                        break
                    record = WALRecord.deserialize(header + payload)
                    records.append(record)
        except Exception as e: 14133.m.kuaisou.com 
            logger.error(f"WAL replay failed: {e}")
        return records

    def truncate(self): 14383.m.kuaisou.com 
        with self._lock: 14145.m.kuaisou.com 
            self._buffer.clear()
            if self.wal_path.exists(): 14384.m.kuaisou.com 
                self.wal_path.unlink()


# ==================== 事务管理器 ====================

class TransactionState(Enum): 15002.m.kuaisou.com 
    ACTIVE = "active"
    COMMITTED = "committed"
    ABORTED = "aborted"


@dataclass
class Transaction: 15015.m.kuaisou.com 
    txn_id: str
    state: TransactionState = TransactionState.ACTIVE
    write_set: Dict[str, KVEntry] = field(default_factory=dict)
    delete_set: set = field(default_factory=set)
    read_set: set = field(default_factory=set)
    start_time: float = field(default_factory=time.time)


class TransactionManager:
    """事务管理器"""

    def __init__(self): 15025.m.kuaisou.com 
        self._transactions: Dict[str, Transaction] = {}
        self._lock = threading.Lock()

    def begin(self, txn_id: str) -> Transaction:
        with self._lock: 15027.m.kuaisou.com 
            txn = Transaction(txn_id=txn_id)
            self._transactions[txn_id] = txn
            return txn

    def get(self, txn_id: str) -> Optional[Transaction]:
        with self._lock: 15029.m.kuaisou.com 
            return self._transactions.get(txn_id)

    def commit(self, txn_id: str) -> bool:
        with self._lock: 15030.m.kuaisou.com 
            txn = self._transactions.get(txn_id)
            if not txn or txn.state != TransactionState.ACTIVE:
                return False
            txn.state = TransactionState.COMMITTED
            return True

    def rollback(self, txn_id: str) -> bool:
        with self._lock: 15031.m.kuaisou.com 
            txn = self._transactions.get(txn_id)
            if not txn or txn.state != TransactionState.ACTIVE:
                return False
            txn.state = TransactionState.ABORTED
            return True

    def cleanup(self, txn_id: str):
        with self._lock: 15032.m.kuaisou.com 
            self._transactions.pop(txn_id, None)


# ==================== KV 存储引擎主体 ====================

class KVStore: 15033.m.kuaisou.com 
    """高性能内存键值存储引擎"""

    def __init__(
        self,
        data_dir: str = DEFAULT_DATA_DIR,
        lru_capacity: int = DEFAULT_LRU_CAPACITY,
        max_level: int = DEFAULT_MAX_LEVEL,
        wal_sync_interval: float = DEFAULT_WAL_SYNC_INTERVAL,
    ):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)

        self.index = SkipList(max_level=max_level)
        self.cache = LRUCache(capacity=lru_capacity)
        self.wal = WALManager(self.data_dir / DEFAULT_WAL_FILE)
        self.wal._sync_interval = wal_sync_interval
        self.txn_manager = TransactionManager(15034.m.kuaisou.com)

        self._rw_lock = threading.RLock(15035.m.kuaisou.com)
        self._ttl_thread: Optional[threading.Thread] = None
        self._running = False

        self.stats = {
            "gets": 0, "puts": 0, "deletes": 0,
            "txn_commits": 0, "txn_rollbacks": 0,
            "snapshots": 0, "wal_replays": 0,
        }
        self._stats_lock = threading.Lock(15036.m.kuaisou.com)

        self._recover(15038.m.kuaisou.com)

    def _inc_stat(self, key: str, delta: int = 1): 15037.m.kuaisou.com 
        with self._stats_lock:
            self.stats[key] = self.stats.get(key, 0) + delta

    # ---------- 核心 CRUD ----------

    def put(self, key: str, value: Any, ttl: Optional[float] = None, txn_id: Optional[str] = None):
        entry = KVEntry(key=key, value=value, ttl=ttl, txn_id=txn_id)

        if txn_id:
            txn = self.txn_manager.get(txn_id)
            if not txn or txn.state != TransactionState.ACTIVE: 15039.m.kuaisou.com 
                raise ValueError(f"Invalid transaction: {txn_id}")
            txn.write_set[key] = entry
            txn.delete_set.discard(key)
            self.wal.append(WALRecord(op_type=WAL_OP_PUT, key=key, value=value, txn_id=txn_id, ttl=ttl))
            return

        with self._rw_lock: 16001.m.kuaisou.com 
            self.index.insert(key, entry)
            self.cache.put(key, entry)
            self.wal.append(WALRecord(op_type=WAL_OP_PUT, key=key, value=value, ttl=ttl))
            self._inc_stat("puts")

    def get(self, key: str, txn_id: Optional[str] = None) -> Optional[Any]:
        self._inc_stat("gets")

        if txn_id: 16002.m.kuaisou.com 
            txn = self.txn_manager.get(txn_id)
            if txn and txn.state == TransactionState.ACTIVE: 16004.m.kuaisou.com 
                if key in txn.delete_set: 16003.m.kuaisou.com 
                    return None
                if key in txn.write_set: 16005.m.kuaisou.com 
                    return txn.write_set[key].value
                txn.read_set.add(key)

        cached = self.cache.get(key)
        if cached is not None: 16006.m.kuaisou.com 
            return cached.value

        with self._rw_lock: 16007.m.kuaisou.com 
            entry = self.index.search(key)
            if entry is None or entry.deleted or entry.is_expired:
                return None
            self.cache.put(key, entry)
            return entry.value

    def delete(self, key: str, txn_id: Optional[str] = None) -> bool:
        if txn_id: 16008.m.kuaisou.com 
            txn = self.txn_manager.get(txn_id)
            if not txn or txn.state != TransactionState.ACTIVE:
                raise ValueError(f"Invalid transaction: {txn_id}")
            txn.delete_set.add(key)
            txn.write_set.pop(key, None)
            self.wal.append(WALRecord(op_type=WAL_OP_DELETE, key=key, txn_id=txn_id))
            return True

        with self._rw_lock: 16009.m.kuaisou.com 
            entry = self.index.delete(key)
            self.cache.remove(key)
            self.wal.append(WALRecord(op_type=WAL_OP_DELETE, key=key))
            self._inc_stat("deletes")
            return entry is not None

    def exists(self, key: str) -> bool: 16010.m.kuaisou.com 
        return self.get(key) is not None

    def range_query(self, start_key: str, end_key: str) -> List[Tuple[str, Any]]:
        with self._rw_lock: 16011.m.kuaisou.com 
            entries = self.index.range_scan(start_key, end_key)
            return [(e.key, e.value) for e in entries]

    # ---------- 事务 ----------

    def begin_transaction(self) -> str: 16012.m.kuaisou.com 
        txn_id = hashlib.md5(f"{time.time()}{random.random()}".encode()).hexdigest()[:12]
        self.txn_manager.begin(txn_id)
        self.wal.append(WALRecord(op_type=WAL_OP_TXN_BEGIN, txn_id=txn_id))
        return txn_id

    def commit_transaction(self, txn_id: str) -> bool: 16013.m.kuaisou.com 
        txn = self.txn_manager.get(txn_id)
        if not txn or txn.state != TransactionState.ACTIVE: 16014.m.kuaisou.com 
            return False

        with self._rw_lock: 16016.m.kuaisou.com 
            for key, entry in txn.write_set.items(): 16015.m.kuaisou.com 
                self.index.insert(key, entry)
                self.cache.put(key, entry)
            for key in txn.delete_set:
                self.index.delete(key)
                self.cache.remove(key)

        self.txn_manager.commit(txn_id)
        self.wal.append(WALRecord(op_type=WAL_OP_TXN_COMMIT, txn_id=txn_id))
        self.txn_manager.cleanup(txn_id)
        self._inc_stat("txn_commits")
        return True

    def rollback_transaction(self, txn_id: str) -> bool: 16017.m.kuaisou.com 
        result = self.txn_manager.rollback(txn_id)
        if result: 16018.m.kuaisou.com 
            self.wal.append(WALRecord(op_type=WAL_OP_TXN_ROLLBACK, txn_id=txn_id))
            self.txn_manager.cleanup(txn_id)
            self._inc_stat("txn_rollbacks")
        return result

    # ---------- 持久化 ----------

    def snapshot(self) -> str:
        with self._rw_lock: 16019.m.kuaisou.com 
            entries = self.index.all_entries()
            data = [e.to_dict() for e in entries if not e.is_expired]

        snapshot_path = self.data_dir / DEFAULT_SNAPSHOT_FILE
        tmp_path = snapshot_path.with_suffix(".tmp")
        try: 16020.m.kuaisou.com 
            tmp_path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
            tmp_path.replace(snapshot_path)
            self.wal.truncate()
            self._inc_stat("snapshots")
            logger.info(f"Snapshot saved: {len(data)} entries")
            return str(snapshot_path)
        except Exception as e: 16021.m.kuaisou.com 
            logger.error(f"Snapshot failed: {e}")
            raise

    def _recover(self): 16022.m.kuaisou.com 
        snapshot_path = self.data_dir / DEFAULT_SNAPSHOT_FILE
        if snapshot_path.exists():
            try: 16023.m.kuaisou.com 
                data = json.loads(snapshot_path.read_text(encoding="utf-8"))
                for item in data: 16024.m.kuaisou.com 
                    entry = KVEntry.from_dict(item)
                    if not entry.is_expired: 16025.m.kuaisou.com 
                        self.index.insert(entry.key, entry)
                        self.cache.put(entry.key, entry)
                logger.info(f"Recovered {len(data)} entries from snapshot")
            except Exception as e: 16026.m.kuaisou.com 
                logger.error(f"Snapshot recovery failed: {e}")

        wal_records = self.wal.replay(16027.m.kuaisou.com)
        if wal_records:
            txns: Dict[str, Dict] = {}
            for record in wal_records: 16028.m.kuaisou.com 
                if record.op_type == WAL_OP_PUT: 16029.m.kuaisou.com 
                    if record.txn_id: 16030.m.kuaisou.com 
                        txns.setdefault(record.txn_id, {"writes": {}, "deletes": set()})
                        txns[record.txn_id]["writes"][record.key] = KVEntry(
                            key=record.key, value=record.value, ttl=record.ttl, txn_id=record.txn_id
                        )
                    else: 16032.m.kuaisou.com 
                        entry = KVEntry(key=record.key, value=record.value, ttl=record.ttl)
                        self.index.insert(record.key, entry)
                        self.cache.put(record.key, entry)
                elif record.op_type == WAL_OP_DELETE: 16031.m.kuaisou.com 
                    if record.txn_id: 16033.m.kuaisou.com 
                        txns.setdefault(record.txn_id, {"writes": {}, "deletes": set()})
                        txns[record.txn_id]["deletes"].add(record.key)
                    else: 16034.m.kuaisou.com 
                        self.index.delete(record.key)
                        self.cache.remove(record.key)
                elif record.op_type == WAL_OP_TXN_COMMIT:
                    txn_data = txns.pop(record.txn_id, None)
                    if txn_data: 16035.m.kuaisou.com 
                        for k, v in txn_data["writes"].items():
                            self.index.insert(k, v)
                            self.cache.put(k, v)
                        for k in txn_data["deletes"]:
                            self.index.delete(k)
                            self.cache.remove(k)
                elif record.op_type == WAL_OP_TXN_ROLLBACK:
                    txns.pop(record.txn_id, None)

            self._inc_stat("wal_replays", len(wal_records))
            logger.info(f"Replayed {len(wal_records)} WAL records")

    # ---------- TTL 清理 ----------

    def start_ttl_checker(self, interval: float = DEFAULT_TTL_CHECK_INTERVAL):
        self._running = True
        self._ttl_thread = threading.Thread(target=self._ttl_loop, args=(interval,), daemon=True)
        self._ttl_thread.start(16036.m.kuaisou.com)
        logger.info(f"TTL checker started (interval={interval}s)")

    def stop_ttl_checker(self): 16037.m.kuaisou.com 
        self._running = False
        if self._ttl_thread:
            self._ttl_thread.join(timeout=5.0)

    def _ttl_loop(self, interval: float): 16038.m.kuaisou.com 
        while self._running: 16039.m.kuaisou.com 
            time.sleep(interval)
            self._cleanup_expired()

    def _cleanup_expired(self):
        with self._rw_lock: 16040.m.kuaisou.com 
            entries = self.index.all_entries()
            expired_keys = [e.key for e in entries if e.is_expired]
            for key in expired_keys: 16041.m.kuaisou.com 
                self.index.delete(key)
                self.cache.remove(key)
            if expired_keys:
                logger.debug(f"Cleaned up {len(expired_keys)} expired keys")

    # ---------- 监控 ----------

    def get_stats(self) -> Dict[str, Any]:
        with self._stats_lock: 16042.m.kuaisou.com 
            stats = dict(self.stats)
        stats["index_size"] = self.index.size
        stats["cache_size"] = len(self.cache._cache)
        stats["cache_hit_rate"] = round(self.cache.hit_rate, 4)
        return stats

    def close(self): 16043.m.kuaisou.com 
        self.stop_ttl_checker()
        self.wal.flush()
        logger.info("KVStore closed")

架构要点说明

  1. 跳表索引:相比平衡树,跳表实现更简单且支持高效的范围扫描。insert/search/delete 平均时间复杂度均为 O(log n),空间复杂度 O(n)。节点使用 __slots__ 减少内存开销。
  2. WAL 预写日志:所有写操作先追加到 WAL,再修改内存索引。WAL 采用定长头+变长体的二进制格式,带 MD5 校验防止损坏。定期 fsync 保证持久性,快照后截断 WAL 避免无限增长。
  3. LRU 缓存:作为热数据加速层,与跳表索引解耦。读操作优先查缓存,命中则直接返回;未命中再查索引并回填缓存。写操作同时更新两者保持一致性。
  4. 事务模型:采用缓冲写入策略,事务内的写操作暂存于 write_set/delete_set,提交时批量应用到主索引。回滚只需丢弃缓冲区,无需反向操作。WAL 中记录事务边界,崩溃恢复时仅重放已提交事务。
  5. TTL 过期清理:后台线程周期性扫描索引,惰性删除过期键。读取时也检查过期状态,双重保障。
  6. 并发安全:读写锁保护索引和缓存的全局一致性;WAL 和事务管理器各自持有独立锁,避免热点竞争。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 在分布式系统、缓存服务与嵌入式数据库中,键值存储是最基础的数据结构。下面从零实现一个生产级内存 KV 存储引擎,覆盖跳表索引、WAL 预写日志、LRU 淘汰策略、快照持久化、事务支持与并发控制。代码量充足、格式标准,适合作为工程化参考。
  • 架构要点说明
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档