Go原生map非并发安全,sync.Map不适用于数据库场景;应基于map[string]*entry+读写分离锁+原子版本/过期控制实现轻量内存引擎。

为什么不用 map 直接当内存数据库?
直接用 map[string]interface{} 存数据看起来最简单,但实际一压测就崩——并发写入会 panic,读写混用时数据可能丢失,甚至 range 遍历时 panic。Go 的原生 map 不是并发安全的,加锁又容易成为性能瓶颈。真正要扛住每秒几万次操作,得从底层控制读写路径和内存布局。
sync.Map 能不能直接用?
能跑通,但不推荐作为核心存储结构。它适合「读多写少 + key 固定」的缓存场景,比如配置表;但对数据库这种高频增删改查、需要事务语义、支持 TTL 和范围查询的场景,sync.Map 缺少原子性操作(比如 CAS 更新 + 过期时间重置)、不支持迭代快照、无法做批量删除或前缀扫描。你很快会发现它只是把问题往后推了:业务逻辑里堆满 LoadOrStore 和类型断言,出错难定位。
自己实现一个最小可行内存引擎的关键点
不是重造 LevelDB,而是聚焦三个刚性需求:线程安全写入、带版本/过期的单 key 操作、O(1) 读取延迟。实操建议如下:
- 底层仍用
map[string]*entry,但所有写操作走统一的writeBatch接口,内部用sync.RWMutex—— 注意只在写 batch 提交时加写锁,读操作全程无锁(靠原子指针替换整个 map) - 每个
*entry包含value、expireAt(纳秒时间戳)、version(uint64,用atomic.AddUint64递增),避免用time.Time带来分配开销 - 删除不真删,设
expireAt = 0标记为逻辑删除;后台用轻量 goroutine 定期扫描(非阻塞,每次只扫 100 个 key),配合runtime.GC()触发时机控制内存回收节奏 - TTL 设置统一走
Set(key, value, ttl time.Duration),内部转成绝对时间戳存,避免每次读都算相对时间
怎么暴露接口才不容易被误用?
别暴露底层 map 或锁对象。对外只提供明确语义的方法:
-
Get(key string) (value interface{}, found bool)—— 不返回 error,未命中就是found == false -
Set(key string, value interface{}, ttl time.Duration)—— ttl -
CompareAndSet(key string, oldVal, newVal interface{}, ttl time.Duration) bool—— 基于 version 实现乐观锁,避免 ABA 问题 -
Keys(prefix string) []string—— 前缀扫描必须拷贝 key 列表返回,禁止返回内部 slice 引用
特别注意:所有方法都不接受 context.Context。内存操作本不该超时,加 context 只会让调用方产生「这个 DB 支持网络等待」的错觉,反而掩盖真实瓶颈。
真正的复杂点不在数据结构,而在边界:比如 Set 时传入 nil 值是否允许、Get 返回的 value 是否保证不可变、并发调用 Keys 和 Delete 时前缀列表是否包含已删 key——这些细节不写进文档,光靠注释根本没人看。











