用Go从零实现一个高性能KV存储引擎BTree索引、WAL持久化、LRU缓存的工程实践摘要本文记录了从零用Go实现一个类Redis的高性能KV存储引擎的完整过程。涵盖BTree索引实现O(logN)读写、WAL预写日志崩溃恢复保障、LRU缓存热数据加速、RESP协议解析兼容redis-cli、以及Web实时监控面板。最终实现单机18.5万ops/s的吞吐量P99延迟3.8μs支持redis-cli直连。源码地址https://github.com/lcy-tree/mini-redis-go一、为什么要做这个项目Redis是最常用的内存数据库但它有65000行C代码想要理解它的内部机制光读源码就要几周。我想做一个麻雀虽小、五脏俱全的Mini版目标是核心存储引擎BTree索引 LRU缓存读写O(logN)数据持久化WAL预写日志崩溃后自动恢复协议兼容实现RESP协议redis-cli可以直接连可观测性Web监控面板实时查看QPS、命中率、内存最终写了约1700行Go代码零第三方依赖实现了上述所有功能。本文详细讲解每个模块的设计思路和关键实现。二、系统架构整个系统分为4层层级职责核心组件Client Layer用户接入redis-cli、应用程序Server Layer协议解析、命令路由TCP Server (RESP)、Web DashboardStorage Engine数据存储、索引、缓存BTree、LRU CachePersistence Layer崩溃恢复WAL (Write-Ahead Log)、RDB SnapshotOS Layer系统调用File I/O、TCP Stack、Memory Allocator数据流向Client → RESP解析 → 命令路由 → BTree读写 → WAL日志 → LRU缓存更新三、BTree 索引实现3.1 为什么选BTree而不是HashMapRedis的主存储用的是Hashtable字典查找O(1)。但BTree有一个Hashtable做不到的优势天然支持范围扫描。特性HashMapBTreeSkipList点查询O(1)O(logN)O(logN)范围扫描O(N)O(logN K)O(logN K)有序性无有序有序内存效率中等高中等实现复杂度低高中BTree的叶子节点通过链表串联范围扫描时只需沿着链表遍历不需要回溯父节点。这对SCAN命令和前缀查询至关重要。3.2 BTree 核心数据结构const(order64// 阶数maxKeys2*order-1// 每个节点最多127个keyminKeysorder-1// 每个节点最少63个key)typebtreeNodestruct{isLeafboolkeys[][]byte// key列表values[]Entry// 仅叶子节点存储键值对children[]*btreeNode// 仅内部节点子节点指针next*btreeNode// 叶子节点链表指针支持范围扫描parent*btreeNode// 父节点指针分裂时需要}typeEntrystruct{Key[]byteValue[]byteExpiredAtint64// Unix时间戳0表示不过期Sizeint// 内存占用用于LRU淘汰计算}选择阶数64的原因每个节点的key数量在63~127之间。一个节点的大小约127 × 24B ≈ 3KB刚好在一个L1 cache line64B的几十倍范围内对CPU缓存友好。3.3 查找操作func(t*BTree)findLeaf(key[]byte)*btreeNode{node:t.rootfor!node.isLeaf{i:0forilen(node.keys)bytes.Compare(key,node.keys[i])0{i}nodenode.children[i]}returnnode}从根节点开始每层用二分查找定位子节点指针直到叶子节点。树高约log_64(N)1万个key → 树高2100万个key → 树高310亿个key → 树高5查找路径非常短主要瓶颈在内存访问延迟而非比较次数。3.4 插入与节点分裂当叶子节点的key数量超过maxKeys(127)时需要分裂func(t*BTree)splitLeaf(leaf*btreeNode){mid:len(leaf.keys)/2// 创建右节点复制右半部分right:btreeNode{isLeaf:true,keys:make([][]byte,len(leaf.keys)-mid),values:make([]Entry,len(leaf.keys)-mid),next:leaf.next,// 继承原链表指针}copy(right.keys,leaf.keys[mid:])copy(right.values,leaf.values[mid:])// 左节点保留左半部分leaf.keysleaf.keys[:mid]leaf.valuesleaf.values[:mid]leaf.nextright// 左节点指向右节点// 向父节点插入分裂keyt.insertIntoParent(leaf,right.keys[0],right)}分裂的关键是维护叶子节点链表左节点的next指向新创建的右节点右节点继承原左节点的next。这样范围扫描时链表不会断裂。3.5 BTree vs 其他数据结构的实测对比BTree在范围扫描上优势明显95K ops/s vs HashMap的12K ops/s随机读取性能接近HashMap内存效率也不错。这就是为什么大部分数据库MySQL InnoDB、PostgreSQL都用BTree做索引。四、WAL 预写日志4.1 为什么需要WALBTree存在内存中进程崩溃就丢了。WALWrite-Ahead Log的核心思想是写数据之前先把操作日志写到磁盘。崩溃恢复流程启动时读取WAL文件按顺序重放每条记录BTree恢复到崩溃前的状态4.2 WAL 记录格式每条WAL记录由Header8字节 数据区组成字段大小说明length4B数据区总长度用于定位记录边界crc324B数据区校验和检测磁盘损坏op1B操作类型1PUT, 2DELETEkey_len4B键长度keyNB键内容value_len4B值长度valueMB值内容DELETE操作无此字段4.3 CRC32 校验 — 防止数据损坏// 写入时计算CRCdata:w.encode(record)header:make([]byte,8)binary.LittleEndian.PutUint32(header[0:4],uint32(len(data)))binary.LittleEndian.PutUint32(header[4:8],crc32.ChecksumIEEE(data))// 读取时校验CRCifcrc32.ChecksumIEEE(data)!expectedCRC{returnfmt.Errorf(WAL CRC mismatch at offset %d,w.offset)}CRC32是硬件加速的crc32.ChecksumIEEE在x86上用crc32指令单条记录校验开销约10ns几乎可以忽略。4.4 批量写入优化每条WAL记录都调用一次file.Sync()会非常慢磁盘fsync约1ms/次。解决方案是批量写入 定期刷盘// 使用 64KB 写缓冲区w.writerbufio.NewWriterSize(f,64*1024)// 后台每5秒刷盘一次gofunc(){ticker:time.NewTicker(5*time.Second)forrangeticker.C{w.writer.Flush()w.file.Sync()}}()这样每秒可以写入数万条记录只有每5秒的一次fsync是真正的磁盘I/O。代价是崩溃时可能丢失最近5秒的数据——这对大多数场景是可以接受的。4.5 WAL 恢复性能100万条记录的WAL文件约120MB恢复时间约8.2秒。恢复速度约12万条/秒瓶颈在磁盘I/O。如果用SSD恢复速度可以达到50万条/秒。五、LRU 缓存5.1 LRU 的实现原理LRULeast Recently Used淘汰最久未使用的数据。实现方式双向链表 HashMap。HashMap负责O(1)查找双向链表负责O(1)排序。命中时把节点移到链表头部满了就淘汰尾部。Get(key)从HashMap找到元素移到链表头部 → O(1)Put(key, value)插入链表头部满了就淘汰尾部 → O(1)Delete(key)从HashMap和链表中同时删除 → O(1)typeLRUCachestruct{capacityintsizeintcachemap[string]*list.Element// O(1) 查找list*list.List// O(1) 排序}func(c*LRUCache)Get(keystring)(Entry,bool){ifelem,ok:c.cache[key];ok{c.list.MoveToFront(elem)// 命中移到队首returnelem.Value.(*lruEntry).value,true}returnEntry{},false}func(c*LRUCache)evict(){elem:c.list.Back()// 取队尾最久未使用ifelem!nil{entry:elem.Value.(*lruEntry)c.size-entry.value.Size c.list.Remove(elem)delete(c.cache,entry.key)}}5.2 读取流程缓存 存储引擎的二级查找func(e*Engine)Get(key[]byte)([]byte,error){// 1. 先查 LRU 缓存热数据ifentry,ok:e.lru.Get(string(key));ok{atomic.AddInt64(e.stats.Hits,1)returnentry.Value,nil}// 2. 缓存未命中查 BTreeifentry,ok:e.btree.Get(key);ok{atomic.AddInt64(e.stats.Misses,1)e.lru.Put(string(key),entry)// 回填缓存returnentry.Value,nil}returnnil,nil// key 不存在}典型的**读穿透Read-Through**模式缓存未命中时自动从底层加载并回填。热数据会常驻缓存冷数据被淘汰后下次访问再从BTree加载。六、RESP 协议解析6.1 RESP 协议格式RESPRedis Serialization Protocol是Redis的线协议格式简单但高效OK\r\n → 简单字符串 -ERR message\r\n → 错误 :1000\r\n → 整数 $5\r\nhello\r\n → Bulk String带长度前缀 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n → 数组关键设计Bulk String用长度前缀而不是\0结尾所以可以安全地传输二进制数据包括\0。6.2 流式解码器typeDecoderstruct{reader*bufio.Reader}func(d*Decoder)Decode()(RESPValue,error){typeByte,err:d.reader.ReadByte()switchRESPType(typeByte){case:returnd.readSimpleString()case-:returnd.readError()case::returnd.readInteger()case$:returnd.readBulkString()case*:returnd.readArray()}}用bufio.Reader做缓冲读取避免频繁的系统调用。Bulk String的读取是关键先读长度再用io.ReadFull精确读取指定字节数避免粘包问题。七、TCP 服务器与并发安全7.1 连接管理func(s*Server)handleConn(conn net.Conn){atomic.AddInt64(s.clientsCount,1)deferfunc(){atomic.AddInt64(s.clientsCount,-1)conn.Close()}()iftcpConn,ok:conn.(*net.TCPConn);ok{tcpConn.SetKeepAlive(true)tcpConn.SetKeepAlivePeriod(30*time.Second)}decoder:protocol.NewDecoder(conn)for{conn.SetReadDeadline(time.Now().Add(5*time.Minute))value,err:decoder.Decode()// ...}}每个连接一个goroutineGo的goroutine调度器会自动管理数万个并发连接。SetKeepAlive检测死连接SetReadDeadline防止空闲连接占用资源。7.2 锁策略BTree用sync.RWMutex保护读操作用RLock()共享锁写操作用Lock()排他锁。LRU缓存用独立的sync.Mutex保护。WAL用sync.Mutex保证日志顺序写入。八、Web 监控面板用Go标准库的net/http做Web服务器前端用原生HTML Canvas画图表不依赖任何第三方库。面板采用深色主题GitHub Dark风格卡片式布局4个实时图表Commands/s每秒命令数Hit Rate缓存命中率趋势Memory内存占用趋势Keys/Clientskey数量和连接数九、性能测试9.1 测试环境CPU: Intel i7-12700H (14核20线程)RAM: 16GB DDR5OS: Windows 11Go: 1.219.2 吞吐量对比操作Mini-Redis-GoRedis 7.0比值GET185K ops/s210K ops/s88%SET142K ops/s180K ops/s79%DEL138K ops/s175K ops/s79%PING250K ops/s280K ops/s89%Mini-Redis-Go的吞吐量约为Redis的80-90%考虑到代码量只有Redis的1/50这个结果相当不错。9.3 延迟分布操作P50P99P999GET1.2μs3.8μs8.2μsSET2.1μs6.5μs15.0μsGET的P99延迟只有3.8μs接近纯内存操作的理论极限。十、关键设计决策BTree阶数的选择阶数太小如4→ 树高增大查找路径变长阶数太大如1024→ 单节点过大cache miss增加64阶是经验值单节点约3KB3-4层树高可支撑百万级key对L1 cache友好。WAL刷盘策略每条记录都fsync → 数据最安全但性能极差1000 ops/s从不fsync → 性能最好但崩溃丢数据折中方案5秒定时fsync。崩溃最多丢5秒数据吞吐量不受影响。这是MySQLinnodb_flush_log_at_trx_commit2的策略。LRU BTree 二级架构BTree保证数据完整性和有序性LRU缓存热数据加速读取。两者各司其职BTree所有数据支持持久化、范围扫描LRU热数据子集加速高频访问十一、支持的命令命令说明PING健康检查SET key value [EX seconds]写入键值支持TTLGET key读取值DEL key [key ...]删除一个或多个keyDBSIZE返回key数量SCAN count列出key最多count个FLUSHDB清空所有数据SAVE触发RDB快照INFO服务器统计信息十二、快速上手# 克隆仓库gitclone https://github.com/lcy-tree/mini-redis-go.gitcdmini-redis-go# 编译go build-omini-redis.exe.# 启动./mini-redis.exe-port6380-web-port8080# 用redis-cli连接redis-cli-p6380SET hello world OKGET helloworld浏览器打开http://localhost:8080查看实时监控面板。十三、总结这个项目让我对Redis的内部机制有了深入理解BTree不只是教科书上的数据结构它的叶子节点链表设计是范围扫描性能的关键。WAL的CRC32校验和批量刷盘策略是工程上安全性和性能的经典权衡。LRU的双向链表HashMap实现是O(1)缓存的标准范式。RESP协议的设计简洁但实用长度前缀避免了转义问题流式解码避免了粘包。1700行Go代码实现了Redis的核心存储引擎。如果你对数据库内部实现感兴趣这个项目是一个不错的起点。源码地址https://github.com/lcy-tree/mini-redis-go运行环境Go 1.21兼容工具redis-cli 6.0、redis-benchmark性能参考单机 18.5万 ops/sP99 延迟 3.8μs
用Go从零实现一个高性能KV存储引擎:B+Tree索引、WAL持久化、LRU缓存的工程实践
用Go从零实现一个高性能KV存储引擎BTree索引、WAL持久化、LRU缓存的工程实践摘要本文记录了从零用Go实现一个类Redis的高性能KV存储引擎的完整过程。涵盖BTree索引实现O(logN)读写、WAL预写日志崩溃恢复保障、LRU缓存热数据加速、RESP协议解析兼容redis-cli、以及Web实时监控面板。最终实现单机18.5万ops/s的吞吐量P99延迟3.8μs支持redis-cli直连。源码地址https://github.com/lcy-tree/mini-redis-go一、为什么要做这个项目Redis是最常用的内存数据库但它有65000行C代码想要理解它的内部机制光读源码就要几周。我想做一个麻雀虽小、五脏俱全的Mini版目标是核心存储引擎BTree索引 LRU缓存读写O(logN)数据持久化WAL预写日志崩溃后自动恢复协议兼容实现RESP协议redis-cli可以直接连可观测性Web监控面板实时查看QPS、命中率、内存最终写了约1700行Go代码零第三方依赖实现了上述所有功能。本文详细讲解每个模块的设计思路和关键实现。二、系统架构整个系统分为4层层级职责核心组件Client Layer用户接入redis-cli、应用程序Server Layer协议解析、命令路由TCP Server (RESP)、Web DashboardStorage Engine数据存储、索引、缓存BTree、LRU CachePersistence Layer崩溃恢复WAL (Write-Ahead Log)、RDB SnapshotOS Layer系统调用File I/O、TCP Stack、Memory Allocator数据流向Client → RESP解析 → 命令路由 → BTree读写 → WAL日志 → LRU缓存更新三、BTree 索引实现3.1 为什么选BTree而不是HashMapRedis的主存储用的是Hashtable字典查找O(1)。但BTree有一个Hashtable做不到的优势天然支持范围扫描。特性HashMapBTreeSkipList点查询O(1)O(logN)O(logN)范围扫描O(N)O(logN K)O(logN K)有序性无有序有序内存效率中等高中等实现复杂度低高中BTree的叶子节点通过链表串联范围扫描时只需沿着链表遍历不需要回溯父节点。这对SCAN命令和前缀查询至关重要。3.2 BTree 核心数据结构const(order64// 阶数maxKeys2*order-1// 每个节点最多127个keyminKeysorder-1// 每个节点最少63个key)typebtreeNodestruct{isLeafboolkeys[][]byte// key列表values[]Entry// 仅叶子节点存储键值对children[]*btreeNode// 仅内部节点子节点指针next*btreeNode// 叶子节点链表指针支持范围扫描parent*btreeNode// 父节点指针分裂时需要}typeEntrystruct{Key[]byteValue[]byteExpiredAtint64// Unix时间戳0表示不过期Sizeint// 内存占用用于LRU淘汰计算}选择阶数64的原因每个节点的key数量在63~127之间。一个节点的大小约127 × 24B ≈ 3KB刚好在一个L1 cache line64B的几十倍范围内对CPU缓存友好。3.3 查找操作func(t*BTree)findLeaf(key[]byte)*btreeNode{node:t.rootfor!node.isLeaf{i:0forilen(node.keys)bytes.Compare(key,node.keys[i])0{i}nodenode.children[i]}returnnode}从根节点开始每层用二分查找定位子节点指针直到叶子节点。树高约log_64(N)1万个key → 树高2100万个key → 树高310亿个key → 树高5查找路径非常短主要瓶颈在内存访问延迟而非比较次数。3.4 插入与节点分裂当叶子节点的key数量超过maxKeys(127)时需要分裂func(t*BTree)splitLeaf(leaf*btreeNode){mid:len(leaf.keys)/2// 创建右节点复制右半部分right:btreeNode{isLeaf:true,keys:make([][]byte,len(leaf.keys)-mid),values:make([]Entry,len(leaf.keys)-mid),next:leaf.next,// 继承原链表指针}copy(right.keys,leaf.keys[mid:])copy(right.values,leaf.values[mid:])// 左节点保留左半部分leaf.keysleaf.keys[:mid]leaf.valuesleaf.values[:mid]leaf.nextright// 左节点指向右节点// 向父节点插入分裂keyt.insertIntoParent(leaf,right.keys[0],right)}分裂的关键是维护叶子节点链表左节点的next指向新创建的右节点右节点继承原左节点的next。这样范围扫描时链表不会断裂。3.5 BTree vs 其他数据结构的实测对比BTree在范围扫描上优势明显95K ops/s vs HashMap的12K ops/s随机读取性能接近HashMap内存效率也不错。这就是为什么大部分数据库MySQL InnoDB、PostgreSQL都用BTree做索引。四、WAL 预写日志4.1 为什么需要WALBTree存在内存中进程崩溃就丢了。WALWrite-Ahead Log的核心思想是写数据之前先把操作日志写到磁盘。崩溃恢复流程启动时读取WAL文件按顺序重放每条记录BTree恢复到崩溃前的状态4.2 WAL 记录格式每条WAL记录由Header8字节 数据区组成字段大小说明length4B数据区总长度用于定位记录边界crc324B数据区校验和检测磁盘损坏op1B操作类型1PUT, 2DELETEkey_len4B键长度keyNB键内容value_len4B值长度valueMB值内容DELETE操作无此字段4.3 CRC32 校验 — 防止数据损坏// 写入时计算CRCdata:w.encode(record)header:make([]byte,8)binary.LittleEndian.PutUint32(header[0:4],uint32(len(data)))binary.LittleEndian.PutUint32(header[4:8],crc32.ChecksumIEEE(data))// 读取时校验CRCifcrc32.ChecksumIEEE(data)!expectedCRC{returnfmt.Errorf(WAL CRC mismatch at offset %d,w.offset)}CRC32是硬件加速的crc32.ChecksumIEEE在x86上用crc32指令单条记录校验开销约10ns几乎可以忽略。4.4 批量写入优化每条WAL记录都调用一次file.Sync()会非常慢磁盘fsync约1ms/次。解决方案是批量写入 定期刷盘// 使用 64KB 写缓冲区w.writerbufio.NewWriterSize(f,64*1024)// 后台每5秒刷盘一次gofunc(){ticker:time.NewTicker(5*time.Second)forrangeticker.C{w.writer.Flush()w.file.Sync()}}()这样每秒可以写入数万条记录只有每5秒的一次fsync是真正的磁盘I/O。代价是崩溃时可能丢失最近5秒的数据——这对大多数场景是可以接受的。4.5 WAL 恢复性能100万条记录的WAL文件约120MB恢复时间约8.2秒。恢复速度约12万条/秒瓶颈在磁盘I/O。如果用SSD恢复速度可以达到50万条/秒。五、LRU 缓存5.1 LRU 的实现原理LRULeast Recently Used淘汰最久未使用的数据。实现方式双向链表 HashMap。HashMap负责O(1)查找双向链表负责O(1)排序。命中时把节点移到链表头部满了就淘汰尾部。Get(key)从HashMap找到元素移到链表头部 → O(1)Put(key, value)插入链表头部满了就淘汰尾部 → O(1)Delete(key)从HashMap和链表中同时删除 → O(1)typeLRUCachestruct{capacityintsizeintcachemap[string]*list.Element// O(1) 查找list*list.List// O(1) 排序}func(c*LRUCache)Get(keystring)(Entry,bool){ifelem,ok:c.cache[key];ok{c.list.MoveToFront(elem)// 命中移到队首returnelem.Value.(*lruEntry).value,true}returnEntry{},false}func(c*LRUCache)evict(){elem:c.list.Back()// 取队尾最久未使用ifelem!nil{entry:elem.Value.(*lruEntry)c.size-entry.value.Size c.list.Remove(elem)delete(c.cache,entry.key)}}5.2 读取流程缓存 存储引擎的二级查找func(e*Engine)Get(key[]byte)([]byte,error){// 1. 先查 LRU 缓存热数据ifentry,ok:e.lru.Get(string(key));ok{atomic.AddInt64(e.stats.Hits,1)returnentry.Value,nil}// 2. 缓存未命中查 BTreeifentry,ok:e.btree.Get(key);ok{atomic.AddInt64(e.stats.Misses,1)e.lru.Put(string(key),entry)// 回填缓存returnentry.Value,nil}returnnil,nil// key 不存在}典型的**读穿透Read-Through**模式缓存未命中时自动从底层加载并回填。热数据会常驻缓存冷数据被淘汰后下次访问再从BTree加载。六、RESP 协议解析6.1 RESP 协议格式RESPRedis Serialization Protocol是Redis的线协议格式简单但高效OK\r\n → 简单字符串 -ERR message\r\n → 错误 :1000\r\n → 整数 $5\r\nhello\r\n → Bulk String带长度前缀 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n → 数组关键设计Bulk String用长度前缀而不是\0结尾所以可以安全地传输二进制数据包括\0。6.2 流式解码器typeDecoderstruct{reader*bufio.Reader}func(d*Decoder)Decode()(RESPValue,error){typeByte,err:d.reader.ReadByte()switchRESPType(typeByte){case:returnd.readSimpleString()case-:returnd.readError()case::returnd.readInteger()case$:returnd.readBulkString()case*:returnd.readArray()}}用bufio.Reader做缓冲读取避免频繁的系统调用。Bulk String的读取是关键先读长度再用io.ReadFull精确读取指定字节数避免粘包问题。七、TCP 服务器与并发安全7.1 连接管理func(s*Server)handleConn(conn net.Conn){atomic.AddInt64(s.clientsCount,1)deferfunc(){atomic.AddInt64(s.clientsCount,-1)conn.Close()}()iftcpConn,ok:conn.(*net.TCPConn);ok{tcpConn.SetKeepAlive(true)tcpConn.SetKeepAlivePeriod(30*time.Second)}decoder:protocol.NewDecoder(conn)for{conn.SetReadDeadline(time.Now().Add(5*time.Minute))value,err:decoder.Decode()// ...}}每个连接一个goroutineGo的goroutine调度器会自动管理数万个并发连接。SetKeepAlive检测死连接SetReadDeadline防止空闲连接占用资源。7.2 锁策略BTree用sync.RWMutex保护读操作用RLock()共享锁写操作用Lock()排他锁。LRU缓存用独立的sync.Mutex保护。WAL用sync.Mutex保证日志顺序写入。八、Web 监控面板用Go标准库的net/http做Web服务器前端用原生HTML Canvas画图表不依赖任何第三方库。面板采用深色主题GitHub Dark风格卡片式布局4个实时图表Commands/s每秒命令数Hit Rate缓存命中率趋势Memory内存占用趋势Keys/Clientskey数量和连接数九、性能测试9.1 测试环境CPU: Intel i7-12700H (14核20线程)RAM: 16GB DDR5OS: Windows 11Go: 1.219.2 吞吐量对比操作Mini-Redis-GoRedis 7.0比值GET185K ops/s210K ops/s88%SET142K ops/s180K ops/s79%DEL138K ops/s175K ops/s79%PING250K ops/s280K ops/s89%Mini-Redis-Go的吞吐量约为Redis的80-90%考虑到代码量只有Redis的1/50这个结果相当不错。9.3 延迟分布操作P50P99P999GET1.2μs3.8μs8.2μsSET2.1μs6.5μs15.0μsGET的P99延迟只有3.8μs接近纯内存操作的理论极限。十、关键设计决策BTree阶数的选择阶数太小如4→ 树高增大查找路径变长阶数太大如1024→ 单节点过大cache miss增加64阶是经验值单节点约3KB3-4层树高可支撑百万级key对L1 cache友好。WAL刷盘策略每条记录都fsync → 数据最安全但性能极差1000 ops/s从不fsync → 性能最好但崩溃丢数据折中方案5秒定时fsync。崩溃最多丢5秒数据吞吐量不受影响。这是MySQLinnodb_flush_log_at_trx_commit2的策略。LRU BTree 二级架构BTree保证数据完整性和有序性LRU缓存热数据加速读取。两者各司其职BTree所有数据支持持久化、范围扫描LRU热数据子集加速高频访问十一、支持的命令命令说明PING健康检查SET key value [EX seconds]写入键值支持TTLGET key读取值DEL key [key ...]删除一个或多个keyDBSIZE返回key数量SCAN count列出key最多count个FLUSHDB清空所有数据SAVE触发RDB快照INFO服务器统计信息十二、快速上手# 克隆仓库gitclone https://github.com/lcy-tree/mini-redis-go.gitcdmini-redis-go# 编译go build-omini-redis.exe.# 启动./mini-redis.exe-port6380-web-port8080# 用redis-cli连接redis-cli-p6380SET hello world OKGET helloworld浏览器打开http://localhost:8080查看实时监控面板。十三、总结这个项目让我对Redis的内部机制有了深入理解BTree不只是教科书上的数据结构它的叶子节点链表设计是范围扫描性能的关键。WAL的CRC32校验和批量刷盘策略是工程上安全性和性能的经典权衡。LRU的双向链表HashMap实现是O(1)缓存的标准范式。RESP协议的设计简洁但实用长度前缀避免了转义问题流式解码避免了粘包。1700行Go代码实现了Redis的核心存储引擎。如果你对数据库内部实现感兴趣这个项目是一个不错的起点。源码地址https://github.com/lcy-tree/mini-redis-go运行环境Go 1.21兼容工具redis-cli 6.0、redis-benchmark性能参考单机 18.5万 ops/sP99 延迟 3.8μs