LMDB 2011 — 把数据库直接 mmap 进内存的嵌入式 KV 存储
待复核LMDB(Lightning Memory-Mapped Database,原名 MDB)是 Howard Chu 2011 年给 OpenLDAP 写的嵌入式键值数据库。核心绝招一句话:把整个数据库文件 mmap 进进程地址空间,读数据库 = 读内存指针。
日常类比:传统数据库像图书馆——你要书时管理员从书库取出,复印一份给你,复印件回收。LMDB 像把整个书库地板掀开搬到你客厅——书一直在原位,你直接走过去翻。
MDB_env *env;MDB_dbi dbi;MDB_val key = {3, "foo"}, data;MDB_txn *txn;
mdb_env_create(&env);mdb_env_open(env, "./mydb", 0, 0664);mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);mdb_get(txn, dbi, &key, &data); // data.mv_data 直接指向 mmap 区data.mv_data 拿到的指针就是数据库文件本身那块内存,没复制、没分配,是真正的 zero-copy。
不理解 LMDB,下面这些事讲不清:
- 为什么 Monero / OpenLDAP / 不少硬件钱包相关栈选 LMDB 而不是 SQLite
- 为什么 OpenLDAP 切到 LMDB 后读延迟从毫秒级直接降到微秒级
- 为什么 etcd v3 用的 boltdb 在设计上明显借鉴了 LMDB(mmap + CoW B+ 树)
- 为什么”无 WAL、无后台线程、崩溃即恢复”在 2011 年是反直觉的设计
LMDB 把简单当成第一性目标,靠三件事撑起 ACID:
-
mmap + OS page cache 当 buffer pool:传统引擎要自己写 LRU buffer pool 管热页冷页,几千行代码。LMDB 直接信任 OS——你 mmap 一段,OS 内核已经在做 LRU,不用重写一遍。
-
Copy-on-Write B+ 树:要改某个值?不就地改。把那条路径上从根到叶的所有页写新副本,最后原子换根指针。类比:改家谱,你不在原谱上涂改,复印一份改完,最后告诉所有人”现在看新谱”。旧版本还在,没人引用时再回收。
-
单写者 + 多读者 MVCC:全局只有一把写锁,同一时刻最多一个写者;读者不用任何锁,只在 reader table 登记自己的 txn ID。读者看到的是开始事务那一刻的快照(指向某棵旧 B+ 树的根),写者改它的不影响。
合起来:没有 WAL(写新页本身就是日志)、没有 compaction(旧页有 free list 自动回收)、崩溃恢复 = 读两个 meta page 选最新有效的(瞬时)。
案例 1:OpenLDAP back-mdb 替换 BerkeleyDB
Section titled “案例 1:OpenLDAP back-mdb 替换 BerkeleyDB”OpenLDAP 早年用 BerkeleyDB(BDB)做后端,BDB 自带 buffer pool / 锁管理 / 事务日志,几万行 C,配置参数几十个,调不好性能能差 10 倍。
Howard Chu 2011 年用 LMDB 重写 back-mdb 后端:
back-bdb 代码量: ~5 万行 C + 几十个调优旋钮back-mdb 代码量: ~7 千行 C + 几乎不用调读吞吐提升 5-10x(很多查询直接命中 OS page cache,不复制不解压),LDAP 服务器从此默认走 LMDB。
案例 2:Monero 用 LMDB 存链上状态
Section titled “案例 2:Monero 用 LMDB 存链上状态”Monero(以及不少资源紧张的币圈节点)把区块链相关 KV 放进 LMDB:读多写少、要 ACID、还想少依赖。读路径大致是:
// 示意:只读事务里按 key 取一块MDB_val key = {...}, data;mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);mdb_get(txn, dbi, &key, &data); // data 指向 mmap 页,零拷贝启动节点时不用预热用户态缓存——mmap 后 OS 按需把热页拉进物理内存。对比:Bitcoin Core 的 chainstate 自 0.8 起用的是 LevelDB;社区有过 LMDB 实验分支,但未合入主线。
案例 3:在 Python 里用 LMDB 做本地缓存
Section titled “案例 3:在 Python 里用 LMDB 做本地缓存”import lmdb
env = lmdb.open('./cache', map_size=10 * 1024**3) # 预留 10GB
with env.begin(write=True) as txn: txn.put(b'user:42', b'{"name": "Ada"}')
with env.begin() as txn: val = txn.get(b'user:42') # 读直接拿 mmap 指针 print(bytes(val)) # 注意:txn 一关,指针就失效要点:读出来的 bytes 在事务关闭后立刻失效——因为它只是指向 mmap 区的指针,事务一结束 LMDB 不保证那块还有效。要长期持有必须 bytes(val) 拷贝一份。
-
map_size 是个上限不是初始值:
mdb_env_set_mapsize必须一次设够(或大)。32 位机器最大 2GB,64 位可以塞 TB 级。设小了写到一半MDB_MAP_FULL,要关 env 重开。 -
长读事务会让磁盘涨:写者每次 CoW 都会产生旧页,等所有读者都不再引用才回收。如果某读者 txn 开了一小时,这一小时所有”旧页”都不能复用,db 文件能涨好几倍。
-
写吞吐天花板低:单写者 = 全局串行写。OLTP 高并发写场景被 RocksDB / LSM 系压得死死的。LMDB 的甜区是”读远多于写”。
-
NOSYNC 是把双刃剑:默认每次 commit 都
fsync,慢但安全。开MDB_NOSYNC速度翻倍但断电会丢最后未刷盘的事务——只在”丢了能重算”的场景用。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 嵌入式 KV / 配置存储 / 本地缓存(钱包、浏览器、IDE)
- 读密集 + 写少(OpenLDAP 查询、UTXO 查询、ML 数据集索引)
- 资源紧张要小代码量(硬件钱包 / IoT 设备)
- 需要 ACID + 多进程共享同一份数据
不适用:
- 写密集高并发 OLTP(用 RocksDB / rocksdb-lsm / 真 RDBMS)
- 数据集远大于 RAM 且热点分散(mmap 优势消失,跟普通 IO 差不多)
- 32 位环境存大数据(2GB 上限)
- 复杂查询 / SQL / 多表 join(这是 SQLite / Postgres 的活)
历史小故事(可跳过)
Section titled “历史小故事(可跳过)”- 2009-2010 年:Howard Chu 在 OpenLDAP 项目里改 BerkeleyDB 后端改到崩溃,BDB 越改越复杂,Oracle 收购后许可也变敏感。
- 2011 年初:Chu 决定从零写。受 1990s Phil Howard 的 append-only 思路 + Doug Lea 的 mmap 文章启发,10 周写完 MDB 雏形。
- 2011 年 8 月:LinuxCon 2011 的 “MDB: A Memory-Mapped Database” 演讲让圈外人首次注意到这个 7 千行的小怪物。
- 2012-2013 年:MDB 改名 LMDB(避免和 Sleepycat MDB 混淆),back-mdb 成 OpenLDAP 默认后端。
- 2014 年起:Mozilla rkv / Monero / Postfix / Cloudflare 等陆续采用;Ben Johnson 用 Go 写出 boltdb(设计灵感来自 LMDB),被 etcd v3 / Consul 等用上。
- 简单是一种性能:LMDB 7 千行 C 跑赢几万行的 BDB——少一层就少一次复制 / 锁 / cache miss。
- 信任 OS:page cache / 文件系统已经在做的事,应用层别重写一遍。
- CoW + 单写者是个被低估的并发组合:实现简单,对读极友好,恰好匹配”读远多于写”的真实负载分布。
- 没 WAL 也能 ACID:写新页换根指针本身就是日志,meta page 双 buffer 让原子提交免费。
- 演讲 PDF:Howard Chu — MDB at LinuxCon 2011
- 设计文档:lmdb.tech 主页(含 doxygen 文档 / 性能对比图)
- 视频:Howard Chu 在多个会议讲 LMDB —— 风格直接,吐槽 BDB 不留情面
- 源码:OpenLDAP LMDB git(mdb.c 一个文件 ~10K 行)
- b-tree-1972 —— 树结构的祖宗
- rocksdb-lsm —— 写优化派的对手
- b-tree-1972 —— LMDB 的索引就是经典 B+ 树,只是页面 CoW
- aries-1992 —— 传统 WAL + 三阶段恢复,LMDB 用 CoW + meta page 双 buffer 绕开了
- rocksdb-lsm —— 写优化的代表,和 LMDB 是 KV 存储两条主路线
- rocksdb-2017 —— RocksDB 工业实战,对比 LMDB 看清”读派 vs 写派”取舍
- lsm-tree-1996 —— LSM 论文,理解为什么写密集场景 LSM 赢
- sqlite-2022 —— 同样是嵌入式但走 SQL + WAL 路线,对比设计哲学
- bigtable-2006 —— 分布式 KV,更上一层但底层思想相通
- lmdb —— LMDB — 内存映射 KV 库