文章目录
- [《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》](#《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》)
- [第三篇:LMDB 深度解析](#第三篇:LMDB 深度解析)
- 第四部分:Cursor机制与LMDB源码结构分析
- [第十三章 LMDB Cursor深入解析](#第十三章 LMDB Cursor深入解析)
- [13.1 什么是Cursor](#13.1 什么是Cursor)
- [13.2 普通查询 vs Cursor遍历](#13.2 普通查询 vs Cursor遍历)
- [13.3 Cursor为什么快?](#13.3 Cursor为什么快?)
-
- [Leaf Page天然有序](#Leaf Page天然有序)
- [Leaf Page通过链路连接](#Leaf Page通过链路连接)
- mmap直接访问
- [13.4 Cursor常用操作](#13.4 Cursor常用操作)
- [第十四章 LMDB源码架构分析](#第十四章 LMDB源码架构分析)
- [14.1 mdb_env](#14.1 mdb_env)
- [14.2 mdb_txn](#14.2 mdb_txn)
- [14.3 mdb_cursor](#14.3 mdb_cursor)
- [14.4 mdb_page](#14.4 mdb_page)
- [14.5 mdb_node](#14.5 mdb_node)
- [第十五章 LMDB一次Get流程源码分析](#第十五章 LMDB一次Get流程源码分析)
- [第十六章 LMDB一次Put流程分析](#第十六章 LMDB一次Put流程分析)
- [第十七章 LMDB源码核心调用关系](#第十七章 LMDB源码核心调用关系)
- [第十八章 LMDB设计思想总结](#第十八章 LMDB设计思想总结)
- 第五部分:C++使用、批量写入与视觉行业应用
- [第十九章 LMDB C++基本使用](#第十九章 LMDB C++基本使用)
- [19.1 创建数据库环境](#19.1 创建数据库环境)
- [19.2 mapsize是什么意思?](#19.2 mapsize是什么意思?)
- [第二十章 写入数据](#第二十章 写入数据)
-
- [20.1 插入Key-Value](#20.1 插入Key-Value)
- [20.2 写入过程对应LMDB内部](#20.2 写入过程对应LMDB内部)
- [第二十一章 查询数据](#第二十一章 查询数据)
- [第二十二章 删除数据](#第二十二章 删除数据)
- [第二十三章 Cursor遍历](#第二十三章 Cursor遍历)
-
- [23.1 创建Cursor](#23.1 创建Cursor)
- [23.2 从第一条开始](#23.2 从第一条开始)
- [23.3 循环遍历](#23.3 循环遍历)
- [第二十四章 批量写入优化](#第二十四章 批量写入优化)
- 推荐方式
- [第二十五章 LMDB在视觉行业中的应用](#第二十五章 LMDB在视觉行业中的应用)
- [25.1 为什么视觉行业喜欢LMDB?](#25.1 为什么视觉行业喜欢LMDB?)
- [25.2 图片存储](#25.2 图片存储)
- [25.3 点云数据](#25.3 点云数据)
- [25.4 Tensor训练数据](#25.4 Tensor训练数据)
- [第二十六章 LMDB与NAS/对象存储结合](#第二十六章 LMDB与NAS/对象存储结合)
- [第二十七章 LMDB适合什么场景?](#第二十七章 LMDB适合什么场景?)
-
- 非常适合:
-
- [1. 读多写少](#1. 读多写少)
- [2. 大量小文件](#2. 大量小文件)
- [3. 嵌入式数据库](#3. 嵌入式数据库)
- 不适合:
- [第二十八章 LMDB核心总结](#第二十八章 LMDB核心总结)
- LMDB完整架构图
《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》
第三篇:LMDB 深度解析
第四部分:Cursor机制与LMDB源码结构分析
第十三章 LMDB Cursor深入解析
在很多Key-Value数据库中:
查询:
text
key → value
通常是主要操作。
但是在工业视觉、AI训练等场景:
经常需要:
text
遍历全部数据
范围查询
顺序读取
批量处理
例如:
训练集:
image_000001
image_000002
image_000003
...
image_100000
如果每次:
get(key)
效率很低。
LMDB提供:
Cursor
用于高效遍历B+Tree。
13.1 什么是Cursor
Cursor可以理解为:
指向B+Tree中当前位置的迭代器。
例如:
B+Tree:
Root
|
Branch Page
|
+----------+----------+
| | |
Leaf1 Leaf2 Leaf3
Cursor:
Cursor
↓
Leaf2
↓
key=1000
它保存:
- 当前Page
- 当前Node位置
- 父节点路径
13.2 普通查询 vs Cursor遍历
普通查询
查询:
key=1000
每次:
Root
↓
Branch
↓
Leaf
↓
Value
复杂度:
O(logN)
Cursor遍历
第一次:
Root
↓
Leaf1
↓
key=1
下一条:
Cursor++
直接:
key=2
无需重新搜索。
复杂度:
O(1)
13.3 Cursor为什么快?
原因1:
Leaf Page天然有序
B+Tree:
Leaf节点:
key=1
key=2
key=3
key=4
所以:
顺序读取:
CPU Cache Friendly
原因2:
Leaf Page通过链路连接
逻辑:
Leaf1
↓
Leaf2
↓
Leaf3
遍历:
while(cursor.next())
{
process(value);
}
无需:
Root查找
原因3:
mmap直接访问
Cursor获得:
Page地址
实际上:
虚拟地址指针移动
没有:
read()
memcpy()
malloc()
13.4 Cursor常用操作
LMDB:
c
mdb_cursor_get()
主要模式:
MDB_FIRST
移动到第一条:
Cursor
↓
First Key
MDB_NEXT
下一条:
A
↓
B
↓
C
MDB_SET
定位Key:
例如:
camera001
MDB_PREV
反向遍历:
C
↓
B
↓
A
第十四章 LMDB源码架构分析
LMDB源码主要:
lmdb.h
mdb.c
核心对象:
mdb_env
mdb_txn
mdb_cursor
mdb_page
mdb_node
14.1 mdb_env
数据库环境
结构:
c
MDB_env
对应:
一个LMDB数据库实例
例如:
cpp
mdb_env_create(&env);
mdb_env_open(
env,
"./database",
0,
0664
);
内部保存:
text
MDB_env
|
|
+ data.mdb文件句柄
|
|
+ mmap地址
|
|
+ Page大小
|
|
+ 最大Reader数量
|
|
+ Writer锁
可以理解:
mdb_env
=
数据库连接对象
类似:
MySQL:
MYSQL connection
14.2 mdb_txn
Transaction:
事务对象。
结构:
MDB_txn
代表:
一次数据库操作。
例如:
读取:
Begin Read Transaction
↓
get()
↓
Commit
写入:
Begin Write Transaction
↓
put()
↓
Commit
内部保存:
MDB_txn
|
|
+ Transaction ID
|
|
+ Root Page
|
|
+ Dirty Pages
|
|
+ Cursor列表
|
|
+ Allocated Pages
14.3 mdb_cursor
对应:
MDB_cursor
保存:
当前遍历状态。
例如:
Cursor
top=3
page[0]=Root
page[1]=Branch
page[2]=Leaf
key index=10
为什么保存路径?
因为B+Tree:
Root
↓
Branch
↓
Leaf
返回上一层需要知道:
父节点在哪里。
14.4 mdb_page
数据库最基础单位。
结构:
MDB_page
一个Page:
默认:
4096 bytes
包含:
Page Header
Node Array
Data
例如:
Leaf Page:
+----------------+
Page Header
+----------------+
Node1
+----------------+
Node2
+----------------+
Node3
+----------------+
14.5 mdb_node
Node代表:
B+Tree中的一个Key。
结构:
MDB_node
类似:
key-value pair
例如:
Node
key:
image001
value:
Page address
Leaf节点:
保存:
真正Value
Branch节点:
保存:
子Page编号
第十五章 LMDB一次Get流程源码分析
调用:
cpp
mdb_get()
例如:
cpp
mdb_get(
txn,
dbi,
&key,
&data
);
Step1
进入:
mdb_get()
Step2
调用:
mdb_cursor_get()
创建Cursor。
Step3
B+Tree搜索。
流程:
Root Page
↓
Branch Page
↓
Leaf Page
比较:
key
例如:
查:
camera001
Root:
A-M
N-Z
选择:
A-M
进入:
Leaf Page
找到:
camera001
Step4
返回Value地址。
注意:
不是:
malloc()
copy()
而是:
data.mv_data
↓
mmap地址
例如:
cpp
MDB_val data;
data.mv_data
实际:
指向:
data.mdb映射区域
第十六章 LMDB一次Put流程分析
写入:
cpp
mdb_put()
Step1
开启Write Transaction
mdb_txn_begin()
Step2
查找Key位置
Cursor Search
Step3
判断:
是否需要新Page
情况:
已有空间:
修改Leaf
但是:
由于COW:
实际:
复制Page
Step4
创建Dirty Page
例如:
Old Page
Page100
↓
New Dirty Page
Page200
Step5
写入新Node
key
value
Step6
更新B+Tree路径
例如:
旧:
Root
↓
Page100
新:
New Root
↓
Page200
Step7
Commit
mdb_txn_commit()
执行:
Flush Pages
↓
Update Meta Page
第十七章 LMDB源码核心调用关系
整体:
Application
|
|
mdb_env
|
|
mdb_txn
|
|
mdb_cursor
|
|
mdb_page
|
|
mdb_node
|
|
mmap data.mdb
第十八章 LMDB设计思想总结
LMDB源码虽然只有:
约:
1万多行C代码
但是包含:
数据库核心思想:
B+Tree
MVCC
Copy-On-Write
Memory Mapping
ACID
Transaction
它没有:
Buffer Pool
WAL
Background Thread
Compaction
原因:
不是缺少功能。
而是:
设计哲学不同。
LMDB认为:
操作系统已经做好:
缓存
页管理
刷盘
虚拟内存
数据库只需要:
管理数据结构
第五部分:C++使用、批量写入与视觉行业应用
第十九章 LMDB C++基本使用
LMDB提供的是C API。
但是C++项目中通常会进行一层封装。
典型结构:
text
Application
|
LMDB Wrapper
|
LMDB C API
|
data.mdb
19.1 创建数据库环境
LMDB所有操作首先需要创建:
c
MDB_env
它代表:
一个数据库环境。
示例:
cpp
#include <lmdb.h>
#include <iostream>
MDB_env* env = nullptr;
int main()
{
int rc;
// 创建环境
rc = mdb_env_create(&env);
if(rc != 0)
{
std::cout
<< "create env failed";
return -1;
}
// 设置最大数据库大小
mdb_env_set_mapsize(
env,
10ULL * 1024 * 1024 * 1024
);
// 打开数据库目录
rc = mdb_env_open(
env,
"./lmdb_data",
0,
0664
);
if(rc != 0)
{
std::cout
<< "open failed";
return -1;
}
std::cout
<< "LMDB opened";
mdb_env_close(env);
}
19.2 mapsize是什么意思?
LMDB使用:
text
mmap
映射数据库文件。
因此需要提前指定:
最大虚拟空间。
例如:
cpp
mdb_env_set_mapsize(
env,
100GB
);
表示:
允许:
text
data.mdb
最大增长到100GB
注意:
这不是立即分配:
100GB内存。
只是:
虚拟地址空间。
例如:
text
Virtual Memory
0GB
|
|
100GB
真正使用:
只有写入数据部分。
第二十章 写入数据
LMDB所有写操作必须在:
text
Write Transaction
中。
流程:
text
Begin Transaction
|
Put
|
Commit
20.1 插入Key-Value
完整代码:
cpp
#include <lmdb.h>
#include <cstring>
void insert(
MDB_env* env
)
{
MDB_txn* txn;
// 开启事务
mdb_txn_begin(
env,
nullptr,
0,
&txn
);
MDB_dbi dbi;
// 打开默认数据库
mdb_dbi_open(
txn,
nullptr,
0,
&dbi
);
const char* keyStr =
"camera001";
const char* valueStr =
"image_data";
MDB_val key;
key.mv_size =
strlen(keyStr);
key.mv_data =
(void*)keyStr;
MDB_val value;
value.mv_size =
strlen(valueStr);
value.mv_data =
(void*)valueStr;
// 写入
int rc =
mdb_put(
txn,
dbi,
&key,
&value,
0
);
if(rc == 0)
{
// 提交
mdb_txn_commit(txn);
}
else
{
// 回滚
mdb_txn_abort(txn);
}
}
20.2 写入过程对应LMDB内部
代码:
cpp
mdb_put()
对应:
text
mdb_put
↓
Cursor定位
↓
B+Tree查找
↓
Copy-On-Write
↓
创建Dirty Page
↓
Commit
↓
更新Meta Page
第二十一章 查询数据
查询:
cpp
mdb_get()
示例:
cpp
void query(
MDB_env* env
)
{
MDB_txn* txn;
mdb_txn_begin(
env,
nullptr,
MDB_RDONLY,
&txn
);
MDB_dbi dbi;
mdb_dbi_open(
txn,
nullptr,
0,
&dbi
);
const char* keyStr =
"camera001";
MDB_val key;
key.mv_size =
strlen(keyStr);
key.mv_data =
(void*)keyStr;
MDB_val value;
int rc =
mdb_get(
txn,
dbi,
&key,
&value
);
if(rc == 0)
{
std::cout
<< "value size:"
<< value.mv_size;
}
mdb_txn_abort(txn);
}
注意:
这里:
cpp
value.mv_data
不是复制的数据。
它直接指向:
text
mmap区域
所以:
读取非常快。
第二十二章 删除数据
删除:
cpp
mdb_del()
示例:
cpp
mdb_del(
txn,
dbi,
&key,
nullptr
);
但是:
删除并不会立即删除Page。
原因:
MVCC。
流程:
text
Delete
↓
旧Page保留
↓
加入Free List
↓
未来复用
第二十三章 Cursor遍历
这是LMDB最强功能之一。
23.1 创建Cursor
cpp
MDB_cursor* cursor;
mdb_cursor_open(
txn,
dbi,
&cursor
);
23.2 从第一条开始
cpp
MDB_val key;
MDB_val value;
int rc;
rc =
mdb_cursor_get(
cursor,
&key,
&value,
MDB_FIRST
);
23.3 循环遍历
cpp
while(rc == 0)
{
std::cout
<< "key size:"
<< key.mv_size;
rc =
mdb_cursor_get(
cursor,
&key,
&value,
MDB_NEXT
);
}
内部:
第一次:
text
Root
↓
Leaf1
↓
key1
下一次:
text
key2
不会重新:
text
Root Search
第二十四章 批量写入优化
视觉行业非常重要。
例如:
训练集:
text
100万张图片
如果:
每张:
一个Transaction。
错误方式:
text
image1
Commit
image2
Commit
image3
Commit
速度非常慢。
原因:
每次:
text
Flush
Meta更新
fsync
推荐方式
批量:
text
Begin Transaction
image1
image2
image3
...
image10000
Commit
示例:
cpp
for(int i=0;i<10000;i++)
{
mdb_put(
txn,
dbi,
&key,
&value,
0
);
}
mdb_txn_commit(txn);
性能提升:
可能:
几十倍。
第二十五章 LMDB在视觉行业中的应用
LMDB在AI领域非常常见。
例如:
Caffe Deep Learning Framework 的训练数据格式:
text
LMDB Dataset
25.1 为什么视觉行业喜欢LMDB?
视觉数据特点:
特点1:大量小文件
例如:
工业检测:
一天:
text
500万张图片
文件:
text
img001.png
img002.png
img003.png
...
普通文件系统:
问题:
text
目录查找
inode
文件打开关闭
随机IO
开销巨大。
LMDB:
变成:
text
dataset.mdb
一个文件。
读取:
text
key
↓
binary image
25.2 图片存储
例如:
Key:
text
camera01_20260808_00001
Value:
text
JPEG Binary
结构:
text
Leaf Page
key
|
|
Overflow Pages
|
|
JPEG Data
读取:
cpp
mdb_get()
↓
返回地址
↓
cv::Mat
例如:
cpp
cv::Mat img(
height,
width,
CV_8UC3,
value.mv_data
);
避免:
text
memcpy
25.3 点云数据
工业3D视觉:
例如:
text
Point Cloud
100万个点
每个点:
cpp
struct Point
{
float x;
float y;
float z;
};
大小:
text
12MB
保存:
Key:
text
scan_00001
Value:
text
binary point array
读取:
cpp
Point* pts =
static_cast<Point*>(
value.mv_data
);
直接访问。
25.4 Tensor训练数据
深度学习:
Tensor:
例如:
text
float32
224*224*3
大小:
text
600KB
保存:
text
key:
sample_001
value:
Tensor binary
训练:
text
DataLoader
↓
LMDB Cursor
↓
Tensor
第二十六章 LMDB与NAS/对象存储结合
实际工业系统:
不会只用LMDB。
常见架构:
text
Camera
|
|
NAS/Object Storage
|
|
Dataset Builder
|
|
LMDB
|
|
Training
例如:
原始数据:
text
NAS
100TB图片
训练前:
转换:
text
LMDB Dataset
500GB
优势:
训练读取速度提升。
第二十七章 LMDB适合什么场景?
非常适合:
1. 读多写少
例如:
AI训练:
text
写一次
读取百万次
2. 大量小文件
例如:
图片:
text
百万级
3. 嵌入式数据库
例如:
设备:
text
配置
历史数据
缓存
不适合:
高频随机写
例如:
订单系统:
text
每秒几十万update
不适合。
原因:
单Writer。
超大规模分布式
例如:
PB级数据。
应该使用:
- Apache Cassandra
- Apache HBase
- TiKV
第二十八章 LMDB核心总结
LMDB本质:
不是一个传统数据库。
它更像:
text
一个超级快的
持久化B+Tree
核心:
text
mmap
↓
直接访问文件
B+Tree
↓
快速索引
Copy-On-Write
↓
无WAL
MVCC
↓
读写隔离
Cursor
↓
高速遍历
LMDB完整架构图
text
Application
|
|
MDB API
|
|
Transaction
|
+------------+------------+
| |
Cursor MVCC
| |
B+Tree Snapshot
|
|
+----+----+
| |
Branch Leaf
|
|
Overflow Page
|
|
mmap
|
|
data.mdb
至此,LMDB核心原理全部介绍完成。