文章目录
- [《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》](#《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》)
- [第三篇:LMDB 深度解析](#第三篇:LMDB 深度解析)
- [第一章 LMDB介绍](#第一章 LMDB介绍)
-
- [1.1 什么是LMDB](#1.1 什么是LMDB)
- [1.2 为什么 OpenLDAP 开发 LMDB](#1.2 为什么 OpenLDAP 开发 LMDB)
- [1.3 LMDB设计目标](#1.3 LMDB设计目标)
- [第二章 LMDB核心特点](#第二章 LMDB核心特点)
- [2.1 Memory Mapping](#2.1 Memory Mapping)
- [2.2 Zero Copy](#2.2 Zero Copy)
- [2.3 ACID事务](#2.3 ACID事务)
-
- [Atomicity 原子性](#Atomicity 原子性)
- [Consistency 一致性](#Consistency 一致性)
- [Isolation 隔离性](#Isolation 隔离性)
- [Durability 持久性](#Durability 持久性)
- [2.4 MVCC](#2.4 MVCC)
- [2.5 Copy-On-Write](#2.5 Copy-On-Write)
- [2.6 为什么LMDB不用WAL](#2.6 为什么LMDB不用WAL)
- [第三章 LMDB整体架构](#第三章 LMDB整体架构)
- LMDB内部结构:
- [第四章 LMDB文件结构深入分析](#第四章 LMDB文件结构深入分析)
- [4.1 data.mdb整体布局](#4.1 data.mdb整体布局)
- [4.2 Meta Page](#4.2 Meta Page)
-
- [为什么需要两个Meta Page?](#为什么需要两个Meta Page?)
- [4.3 Page类型](#4.3 Page类型)
-
- [1. Branch Page](#1. Branch Page)
- [2. Leaf Page](#2. Leaf Page)
- [3. Overflow Page](#3. Overflow Page)
- [4. Free Page](#4. Free Page)
- [第五章 mmap原理](#第五章 mmap原理)
- [5.1 传统读取方式](#5.1 传统读取方式)
- [5.2 mmap读取方式](#5.2 mmap读取方式)
- [5.3 为什么读取速度快?](#5.3 为什么读取速度快?)
-
- 原因1:减少复制
- [原因2:利用OS Page Cache](#原因2:利用OS Page Cache)
- 原因3:CPU缓存友好
- [第六章 LMDB中的B+Tree索引](#第六章 LMDB中的B+Tree索引)
- [6.1 为什么使用B+Tree?](#6.1 为什么使用B+Tree?)
- [6.2 LMDB B+Tree结构](#6.2 LMDB B+Tree结构)
-
- [Root Page](#Root Page)
- [Branch Page](#Branch Page)
- [Leaf Page](#Leaf Page)
- [6.3 为什么没有独立索引?](#6.3 为什么没有独立索引?)
- [6.4 B+Tree查找过程](#6.4 B+Tree查找过程)
- [6.5 为什么LMDB遍历很快?](#6.5 为什么LMDB遍历很快?)
- [第七章 LMDB与传统B+Tree数据库区别](#第七章 LMDB与传统B+Tree数据库区别)
- LMDB核心思想总结
- [第三部分:Copy-On-Write、MVCC、Overflow Page](#第三部分:Copy-On-Write、MVCC、Overflow Page)
- [第八章 Copy-On-Write(写时复制)](#第八章 Copy-On-Write(写时复制))
- [8.1 什么是 Copy-On-Write](#8.1 什么是 Copy-On-Write)
- [8.2 LMDB写事务流程](#8.2 LMDB写事务流程)
-
- [第一步:开启Write Transaction](#第一步:开启Write Transaction)
- 第二步:复制Page
- 第三步:修改新Page
- 第四步:创建新的Root
- 第五步:提交
- [8.3 为什么Copy-On-Write不需要WAL?](#8.3 为什么Copy-On-Write不需要WAL?)
- [第九章 MVCC机制](#第九章 MVCC机制)
- [9.1 为什么需要MVCC?](#9.1 为什么需要MVCC?)
- [9.2 Reader Snapshot](#9.2 Reader Snapshot)
- [9.3 多Reader并发](#9.3 多Reader并发)
- [9.4 为什么一个Writer?](#9.4 为什么一个Writer?)
- [9.5 写事务冲突](#9.5 写事务冲突)
- [第十章 Overflow Page](#第十章 Overflow Page)
- [10.1 普通数据结构](#10.1 普通数据结构)
- [10.2 大Value存储](#10.2 大Value存储)
- [10.3 Overflow Page特点](#10.3 Overflow Page特点)
- [10.4 为什么不用拆成多个Key?](#10.4 为什么不用拆成多个Key?)
- [10.5 视觉行业典型应用](#10.5 视觉行业典型应用)
- [第十一章 Free List机制](#第十一章 Free List机制)
- [第十二章 LMDB事务模型总结](#第十二章 LMDB事务模型总结)
- LMDB核心优势总结
- [第四部分:LMDB Cursor与源码分析](#第四部分:LMDB Cursor与源码分析)
《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》
第三篇:LMDB 深度解析
第一章 LMDB介绍
1.1 什么是LMDB
LMDB(Lightning Memory-Mapped Database)是一款高性能嵌入式 Key-Value 数据库。
官方全称:
Lightning Memory-Mapped Database
中文可以理解为:
基于内存映射技术的高速轻量级数据库。
LMDB 最初由 OpenLDAP Project 团队开发,用于替代传统 Berkeley DB。
它被大量应用于:
- LDAP服务器
- AI训练数据集
- 图像数据库
- 嵌入式系统
- 高性能缓存系统
典型使用场景:
Key
↓
Value
image_001
↓
JPEG二进制数据
pointcloud_001
↓
PLY/LAS数据
tensor_001
↓
float数组
1.2 为什么 OpenLDAP 开发 LMDB
在早期 LDAP 系统中,数据库主要使用:
Berkeley DB
但是随着数据规模增长,出现一些问题。
例如:
问题1:复杂锁机制
传统数据库:
Reader
|
Lock
|
Writer
大量读请求会竞争锁。
问题2:缓存重复
传统数据库:
Disk
↓
Database Cache
↓
Application Buffer
↓
User Memory
一次读取:
数据可能复制多次。
例如:
磁盘数据
100MB
↓
数据库缓存
100MB
↓
应用缓存
100MB
↓
程序对象
100MB
最终:
400MB内存
保存同一份数据。
问题3:事务恢复复杂
传统数据库:
Write Data
↓
WAL日志
↓
Flush
↓
Commit
需要:
- 日志管理
- 崩溃恢复
- checkpoint
维护成本高。
1.3 LMDB设计目标
LMDB设计目标非常明确:
目标1:极致读取性能
利用:
mmap
直接映射文件。
让:
Disk
↓
Virtual Memory
↓
Application
减少数据复制。
目标2:简单可靠事务
采用:
MVCC
+
Copy-On-Write
实现:
- 多读
- 单写
- 无锁读取
目标3:零维护
LMDB没有:
- 后台线程
- Cache管理
- Compaction
- WAL
数据库就是:
一个文件
第二章 LMDB核心特点
LMDB主要由以下几个技术组成:
Memory Mapping
Zero Copy
B+Tree
MVCC
Copy-On-Write
ACID Transaction
2.1 Memory Mapping
核心技术:
mmap()
传统数据库:
Application
|
read()
|
Kernel Buffer
|
Disk
数据路径:
Disk
↓
Kernel Page Cache
↓
Application Buffer
需要复制。
LMDB:
Database File
|
|
mmap
↓
Virtual Memory
↓
Application
应用直接访问文件映射区域。
例如:
数据库:
data.mdb
0x0000
Page0
0x1000
Page1
0x2000
Page2
映射后:
虚拟地址:
0x80000000
↓
Page0
0x80001000
↓
Page1
访问:
cpp
value = ptr[offset];
实际上访问:
磁盘文件页
2.2 Zero Copy
传统数据库:
读取:
Disk
↓
Kernel Buffer
↓
Database Buffer
↓
Application Memory
三次复制。
LMDB:
Disk
↓
Memory Mapping
↓
Application Pointer
数据直接暴露。
例如:
保存图片:
image.jpg
10MB
传统:
read()
↓
malloc(10MB)
↓
copy
LMDB:
const void* ptr;
mdb_get()
↓
返回地址
直接读取。
2.3 ACID事务
LMDB支持完整 ACID:
Atomicity 原子性
事务:
BEGIN
修改A
修改B
COMMIT
如果失败:
全部取消
Consistency 一致性
B+Tree结构始终正确。
Isolation 隔离性
多个Reader:
看到:
自己的Snapshot
Durability 持久性
提交后:
数据最终写入磁盘。
2.4 MVCC
Multi Version Concurrency Control。
多版本并发控制。
核心思想:
不修改旧数据,而是创建新版本。
例如:
原始数据:
Page 10
key=A
value=100
修改:
A=200
不是覆盖:
而是:
Page 10
A=100
Page 20
A=200
然后:
切换Root。
2.5 Copy-On-Write
LMDB最大特色。
传统数据库:
修改Page
↓
覆盖旧Page
↓
写WAL
LMDB:
旧Page
不动
复制
↓
新Page
↓
修改
↓
更新Root
例如:
原始:
Root
|
Page1
|
Data=A
修改:
创建:
New Root
|
New Page1
|
Data=B
旧数据:
仍然存在。
2.6 为什么LMDB不用WAL
传统数据库:
修改数据
↓
写日志
↓
修改数据页
因为:
如果崩溃:
根据日志恢复。
LMDB:
由于:
Copy-On-Write
所以:
旧数据永远存在。
提交过程:
写新Page
↓
flush
↓
修改Root Pointer
如果中途崩溃:
旧Root仍然有效。
恢复:
直接使用旧版本。
因此:
不需要:
Write Ahead Log
第三章 LMDB整体架构
LMDB数据库目录:
mydb/
├── data.mdb
└── lock.mdb
其中:
data.mdb
真正数据文件。
包含:
Meta Page
B+Tree Pages
Data Pages
Free Pages
lock.mdb
用于:
Reader Table
Lock Information
Transaction State
记录:
当前:
Reader数量
Reader ID
LMDB内部结构:
data.mdb
+----------------+
| Meta Page 0 |
+----------------+
| Meta Page 1 |
+----------------+
| Root Page |
+----------------+
| Branch Pages |
+----------------+
| Leaf Pages |
+----------------+
| Overflow Pages |
+----------------+
| Free Pages |
+----------------+
第二部分:文件结构、mmap原理、B+Tree索引
第四章 LMDB文件结构深入分析
LMDB整个数据库非常简单。
一个数据库环境:
my_database/
├── data.mdb
└── lock.mdb
其中:
data.mdb:存储全部数据lock.mdb:管理事务和读写同步
LMDB没有:
日志文件
缓存文件
索引文件
临时文件
所有内容都在:
data.mdb
中。
4.1 data.mdb整体布局
LMDB的数据文件本质:
一个由固定大小 Page 组成的大文件。
默认:
Page Size = 4096 bytes
也就是:
4KB/Page
结构:
data.mdb
+----------------+
| Meta Page 0 |
+----------------+
| Meta Page 1 |
+----------------+
| Branch Page |
+----------------+
| Branch Page |
+----------------+
| Leaf Page |
+----------------+
| Leaf Page |
+----------------+
| Overflow Page |
+----------------+
| Free Page |
+----------------+
为什么使用Page?
因为操作系统:
Virtual Memory
↓
Page
↓
Disk Block
天然以Page为单位管理。
LMDB直接利用操作系统:
OS Page Cache
而不是自己实现缓存。
4.2 Meta Page
LMDB最重要的结构之一:
Meta Page
文件开始位置:
Page 0
Page 1
两个Meta Page。
例如:
data.mdb
Page0
+----------------+
| Meta Version 1 |
| Root=100 |
+----------------+
Page1
+----------------+
| Meta Version 2 |
| Root=200 |
+----------------+
为什么需要两个Meta Page?
因为事务提交不能直接覆盖。
假设:
当前:
Meta0:
Root Page = 100
进行修改:
创建:
New Root = 200
提交:
步骤:
1.
写入新的数据Page
2.
写新的Meta Page
3.
切换Meta指针
如果崩溃:
情况:
New Page 已写入
Meta 未更新
启动:
发现:
Meta0有效
继续使用旧版本。
如果:
Meta1更新成功
启动:
读取:
最新Meta
即可。
所以:
两个Meta Page实现:
Crash Recovery
4.3 Page类型
LMDB主要有几种Page。
1. Branch Page
也叫:
Internal Node
用于:
B+Tree索引。
保存:
Key
Child Page Number
例如:
Root
Branch Page
10 50 90
| | |
Leaf Leaf Leaf
2. Leaf Page
真正保存数据。
例如:
Leaf Page
+----------------+
| key=a |
| value=100 |
+----------------+
| key=b |
| value=200 |
+----------------+
3. Overflow Page
保存超大Value。
例如:
普通Page:
4KB
但是:
Value:
20KB
无法保存。
需要:
Page100
+
Page101
+
Page102
+
Page103
+
Page104
连续Page。
4. Free Page
释放后的Page。
例如:
删除:
key=A
旧Page不会立即覆盖。
进入:
Free List
以后事务重新利用。
第五章 mmap原理
LMDB性能核心:
Memory Mapping
5.1 传统读取方式
例如:
读取:
data.db
100MB数据。
传统:
Application
read()
↓
Kernel Buffer
↓
Disk
数据复制:
第一次:
Disk
↓
Kernel Buffer
第二次:
Kernel Buffer
↓
Application Buffer
产生:
Copy
5.2 mmap读取方式
LMDB:
调用:
cpp
mmap()
建立:
文件
↓
虚拟地址空间
结构:
Disk
data.mdb
|
|
mmap
|
|
Virtual Memory
|
|
Application
例如:
文件:
data.mdb
offset 4096
Page1
映射:
0x10000000
↓
Page1
程序:
cpp
char* ptr;
ptr[10];
CPU访问:
虚拟地址
↓
MMU
↓
Page Table
↓
物理页
↓
磁盘缓存
5.3 为什么读取速度快?
原因:
原因1:减少复制
传统:
Disk
↓
Kernel
↓
DB
↓
Application
LMDB:
Disk
↓
Virtual Memory
↓
Application
原因2:利用OS Page Cache
Linux/Windows已经维护:
热点数据
LMDB不用再次实现:
LRU Cache
原因3:CPU缓存友好
B+Tree节点:
连续Page。
CPU读取:
Cache Line
↓
Memory
效率高。
第六章 LMDB中的B+Tree索引
LMDB虽然叫:
Key-Value数据库。
但是底层:
不是Hash。
而是:
B+Tree
6.1 为什么使用B+Tree?
因为数据库主要操作:
查询
范围查询
排序遍历
例如:
查询:
key=1000
范围:
1000 ~ 2000
B+Tree非常适合。
6.2 LMDB B+Tree结构
结构:
Root
|
Branch Page
/ | \
Page10 Page20 Page30
| | |
Leaf Leaf Leaf
Root Page
保存:
最高层索引。
例如:
A-M
N-Z
Branch Page
保存:
Key
Child Page ID
例如:
Branch
A
↓
Page100
M
↓
Page200
Leaf Page
保存:
真正KV。
例如:
Leaf Page
apple
100
banana
200
cat
300
6.3 为什么没有独立索引?
很多数据库:
例如:
MySQL InnoDB:
结构:
Index
↓
Data
索引和数据分离。
LMDB:
数据本身就是B+Tree。
结构:
B+Tree
Root
↓
Branch
↓
Leaf
Leaf里面就是Value
所以:
没有:
额外Index
优势:
减少一次查找。
传统:
Index
↓
Record
两次访问。
LMDB:
Index + Data
一次完成
6.4 B+Tree查找过程
查询:
key="camera001"
第一步:
Root:
camera001
↓
找到Branch Page
第二步:
Branch:
camera001
↓
Page 500
第三步:
Leaf:
camera001
↓
Value
复杂度:
O(logN)
6.5 为什么LMDB遍历很快?
因为:
B+Tree Leaf节点:
通过链表连接。
例如:
Leaf1
↓
Leaf2
↓
Leaf3
↓
Leaf4
遍历:
Cursor
↓
顺序读取
无需重新搜索。
第七章 LMDB与传统B+Tree数据库区别
| LMDB | MySQL InnoDB | |
|---|---|---|
| 索引 | 数据本身 | 独立索引 |
| 缓存 | OS Page Cache | Buffer Pool |
| 日志 | 无WAL | Redo Log |
| 事务 | Copy-On-Write | Undo/Redo |
| 并发 | MVCC | MVCC |
| 写入 | 单Writer | 多Writer |
| 读取 | 极快 | 快 |
| 适合 | 读多写少 | 高并发业务 |
LMDB核心思想总结
LMDB不是通过:
复杂缓存
大量线程
日志系统
获得性能。
而是:
利用操作系统
+
B+Tree
+
Copy-On-Write
+
MVCC
达到:
极低延迟读取。
第三部分:Copy-On-Write、MVCC、Overflow Page
第八章 Copy-On-Write(写时复制)
Copy-On-Write(COW)是 LMDB 最核心的设计。
它决定了:
- 为什么 LMDB 不需要 WAL
- 为什么读取没有锁
- 为什么事务回滚非常简单
- 为什么数据库恢复速度极快
8.1 什么是 Copy-On-Write
传统数据库:
修改已有数据:
Old Page
↓
直接覆盖
↓
New Data
例如:
原数据:
Page 100
key=A
value=100
修改:
A=200
传统方式:
Page 100
key=A
value=200
问题:
如果写入过程中断电:
Page写了一半
↓
数据损坏
所以需要:
WAL日志
LMDB:
不会修改旧Page。
流程:
Old Page
保留
复制
↓
New Page
↓
修改New Page
8.2 LMDB写事务流程
假设数据库:
Root
|
Page 100
|
key=A
value=100
现在执行:
cpp
put("A",200)
第一步:开启Write Transaction
Writer Transaction
开始
当前:
Root
↓
Page100
第二步:复制Page
创建:
Page200
key=A
value=100
此时:
Old Page100
保留
New Page200
修改
第三步:修改新Page
Page200
key=A
value=200
第四步:创建新的Root
旧:
Root
↓
Page100
新:
New Root
↓
Page200
第五步:提交
提交顺序:
写入New Page
↓
flush磁盘
↓
更新Meta Page
↓
完成commit
最终:
Meta
↓
New Root
↓
Page200
↓
value=200
旧版本:
Page100
value=100
仍然存在。
8.3 为什么Copy-On-Write不需要WAL?
传统数据库:
需要恢复:
日志
↓
重新执行修改
例如:
Redo Log:
修改Page100
value=200
崩溃后:
重新执行。
LMDB:
恢复:
只需要找到:
最新Meta Page
即可。
因为:
数据已经完整写入。
例如:
情况1:
New Page写成功
Meta未更新
启动:
读取旧Meta:
Root=Page100
结果:
旧数据。
情况2:
New Page成功
Meta更新成功
启动:
读取:
Root=Page200
新数据。
所以:
恢复过程:
读取Meta Page
↓
找到Root
↓
继续运行
没有:
日志扫描
Redo
Undo
第九章 MVCC机制
LMDB支持:
Multi Version Concurrency Control
多版本并发控制。
核心:
Reader永远读取一个稳定的数据快照。
9.1 为什么需要MVCC?
数据库同时存在:
Reader
Writer
例如:
线程A:
读取:
key=A
线程B:
修改:
key=A
如果直接覆盖:
Reader可能看到:
半修改数据
LMDB:
使用版本。
9.2 Reader Snapshot
假设:
当前版本:
Transaction ID = 100
Reader打开:
Reader TXN
看到:
Version 100
此时Writer修改:
产生:
Version 101
结构:
Version100
Root
↓
Old Page
value=100
Version101
Root
↓
New Page
value=200
Reader:
仍然:
读取Version100
Writer:
使用:
Version101
两者互不影响。
9.3 多Reader并发
LMDB允许:
Reader1
Reader2
Reader3
Reader4
...
同时读取。
例如:
Database
/ | \
Reader1 Reader2 Reader3
Version100
因为:
Reader不修改数据。
所以:
无需锁。
9.4 为什么一个Writer?
LMDB设计:
多个Reader
+
一个Writer
原因:
写事务涉及:
修改Root
分配Page
更新Free List
如果多个Writer:
需要解决:
Page冲突
Root竞争
Free List竞争
复杂度大幅增加。
因此:
LMDB采用:
Single Writer
换取:
极低复杂度。
9.5 写事务冲突
例如:
两个线程:
Writer A
Writer B
同时:
修改数据库
结果:
Writer A
获得写锁
Writer B
等待
完成:
A Commit
↓
B开始
第十章 Overflow Page
数据库中:
Value大小是不固定的。
例如:
Key:
image001
Value:
JPEG图片
可能:
5MB
普通Leaf Page:
大小:
4KB
无法保存。
所以LMDB设计:
Overflow Page。
10.1 普通数据结构
例如:
Key
Value
大小:
500 Bytes
保存:
Leaf Page
+----------------+
key
value
+----------------+
10.2 大Value存储
例如:
Value:
20KB
Page:
4KB
需要:
20KB / 4KB
≈5 Pages
结构:
Leaf Page
key=image001
overflow pointer
|
↓
Page100
Page101
Page102
Page103
Page104
10.3 Overflow Page特点
连续分配:
Page100
Page101
Page102
原因:
提高:
顺序读取速度
10.4 为什么不用拆成多个Key?
例如:
图片:
20MB
拆:
image001_part1
image001_part2
...
问题:
读取:
需要:
多次查找
多次拼接
效率低。
LMDB:
直接:
一个Value
多个连续Page
10.5 视觉行业典型应用
例如:
工业视觉:
一张图片:
4096×4096
16bit TIFF
≈32MB
存储:
key:
camera001_20260807_001
value:
binary image data
LMDB:
Leaf
key
↓
Overflow Pages
↓
Image
读取:
一次事务
直接映射内存
第十一章 Free List机制
Copy-On-Write产生一个问题:
旧Page怎么办?
例如:
修改:
Page100
↓
生成Page200
那么:
Page100:
已经不用。
不能马上删除。
因为:
Reader可能还在使用。
所以:
LMDB维护:
Free List
结构:
Free List
Page100
Page300
Page500
什么时候回收?
当:
所有Reader
都不再使用旧版本
之后:
这些Page:
重新分配。
第十二章 LMDB事务模型总结
完整流程:
Reader
打开事务
↓
获取Snapshot
↓
读取旧Root
↓
无锁访问
Writer:
Begin Write
↓
Copy-On-Write
↓
生成新Page
↓
更新Root
↓
Commit
↓
新版本生效
整个系统:
Meta Page
|
Root
|
B+Tree
|
Leaf / Overflow
Reader:
读取旧版本
Writer:
创建新版本
LMDB核心优势总结
| 技术 | 作用 |
|---|---|
| mmap | 减少复制,提高读取 |
| B+Tree | 高效索引 |
| Copy-On-Write | 无需WAL |
| MVCC | 读写隔离 |
| Overflow Page | 支持大Value |
| Free List | 空间复用 |
| Single Writer | 降低复杂度 |
下一部分:
第四部分:LMDB Cursor与源码分析
内容:
- Cursor为什么遍历极快
- mdb_env结构
- mdb_txn结构
- mdb_cursor结构
- mdb_page结构
- mdb_node结构
- 从源码角度分析一次put/get流程
见下一篇