《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》-第三篇:LMDB 深度解析二
文章目录《现代 Key-Value 数据库原理从 BTree 到 LSM Tree》第三篇LMDB 深度解析第四部分Cursor机制与LMDB源码结构分析第十三章 LMDB Cursor深入解析13.1 什么是Cursor13.2 普通查询 vs Cursor遍历普通查询Cursor遍历13.3 Cursor为什么快Leaf Page天然有序Leaf Page通过链路连接mmap直接访问13.4 Cursor常用操作MDB_FIRSTMDB_NEXTMDB_SETMDB_PREV第十四章 LMDB源码架构分析14.1 mdb_env数据库环境14.2 mdb_txn14.3 mdb_cursor14.4 mdb_page14.5 mdb_node第十五章 LMDB一次Get流程源码分析Step1Step2Step3Step4第十六章 LMDB一次Put流程分析Step1Step2Step3Step4Step5Step6Step7第十七章 LMDB源码核心调用关系第十八章 LMDB设计思想总结第五部分C使用、批量写入与视觉行业应用第十九章 LMDB C基本使用19.1 创建数据库环境19.2 mapsize是什么意思第二十章 写入数据20.1 插入Key-Value20.2 写入过程对应LMDB内部第二十一章 查询数据第二十二章 删除数据第二十三章 Cursor遍历23.1 创建Cursor23.2 从第一条开始23.3 循环遍历第二十四章 批量写入优化推荐方式第二十五章 LMDB在视觉行业中的应用25.1 为什么视觉行业喜欢LMDB特点1大量小文件25.2 图片存储25.3 点云数据25.4 Tensor训练数据第二十六章 LMDB与NAS/对象存储结合第二十七章 LMDB适合什么场景非常适合1. 读多写少2. 大量小文件3. 嵌入式数据库不适合高频随机写超大规模分布式第二十八章 LMDB核心总结LMDB完整架构图《现代 Key-Value 数据库原理从 BTree 到 LSM Tree》第三篇LMDB 深度解析第四部分Cursor机制与LMDB源码结构分析第十三章 LMDB Cursor深入解析在很多Key-Value数据库中查询key → value通常是主要操作。但是在工业视觉、AI训练等场景经常需要遍历全部数据 范围查询 顺序读取 批量处理例如训练集image_000001 image_000002 image_000003 ... image_100000如果每次get(key)效率很低。LMDB提供Cursor用于高效遍历BTree。13.1 什么是CursorCursor可以理解为指向BTree中当前位置的迭代器。例如BTreeRoot | Branch Page | -------------------- | | | Leaf1 Leaf2 Leaf3CursorCursor ↓ Leaf2 ↓ key1000它保存当前Page当前Node位置父节点路径13.2 普通查询 vs Cursor遍历普通查询查询key1000每次Root ↓ Branch ↓ Leaf ↓ Value复杂度O(logN)Cursor遍历第一次Root ↓ Leaf1 ↓ key1下一条Cursor直接key2无需重新搜索。复杂度O(1)13.3 Cursor为什么快原因1Leaf Page天然有序BTreeLeaf节点key1 key2 key3 key4所以顺序读取CPU Cache Friendly原因2Leaf Page通过链路连接逻辑Leaf1 ↓ Leaf2 ↓ Leaf3遍历while(cursor.next()) { process(value); }无需Root查找原因3mmap直接访问Cursor获得Page地址实际上虚拟地址指针移动没有read() memcpy() malloc()13.4 Cursor常用操作LMDBmdb_cursor_get()主要模式MDB_FIRST移动到第一条Cursor ↓ First KeyMDB_NEXT下一条A ↓ B ↓ CMDB_SET定位Key例如camera001MDB_PREV反向遍历C ↓ B ↓ A第十四章 LMDB源码架构分析LMDB源码主要lmdb.h mdb.c核心对象mdb_env mdb_txn mdb_cursor mdb_page mdb_node14.1 mdb_env数据库环境结构MDB_env对应一个LMDB数据库实例例如mdb_env_create(env);mdb_env_open(env,./database,0,0664);内部保存MDB_env | | data.mdb文件句柄 | | mmap地址 | | Page大小 | | 最大Reader数量 | | Writer锁可以理解mdb_env 数据库连接对象类似MySQLMYSQL connection14.2 mdb_txnTransaction事务对象。结构MDB_txn代表一次数据库操作。例如读取Begin Read Transaction ↓ get() ↓ Commit写入Begin Write Transaction ↓ put() ↓ Commit内部保存MDB_txn | | Transaction ID | | Root Page | | Dirty Pages | | Cursor列表 | | Allocated Pages14.3 mdb_cursor对应MDB_cursor保存当前遍历状态。例如Cursor top3 page[0]Root page[1]Branch page[2]Leaf key index10为什么保存路径因为BTreeRoot ↓ 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_nodeNode代表BTree中的一个Key。结构MDB_node类似key-value pair例如Node key: image001 value: Page addressLeaf节点保存真正ValueBranch节点保存子Page编号第十五章 LMDB一次Get流程源码分析调用mdb_get()例如mdb_get(txn,dbi,key,data);Step1进入mdb_get()Step2调用mdb_cursor_get()创建Cursor。Step3BTree搜索。流程Root Page ↓ Branch Page ↓ Leaf Page比较key例如查camera001RootA-M N-Z选择A-M进入Leaf Page找到camera001Step4返回Value地址。注意不是malloc() copy()而是data.mv_data ↓ mmap地址例如MDB_val data;data.mv_data实际指向data.mdb映射区域第十六章 LMDB一次Put流程分析写入mdb_put()Step1开启Write Transactionmdb_txn_begin()Step2查找Key位置Cursor SearchStep3判断是否需要新Page情况已有空间修改Leaf但是由于COW实际复制PageStep4创建Dirty Page例如Old Page Page100 ↓ New Dirty Page Page200Step5写入新Nodekey valueStep6更新BTree路径例如旧Root ↓ Page100新New Root ↓ Page200Step7Commitmdb_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代码但是包含数据库核心思想BTree MVCC Copy-On-Write Memory Mapping ACID Transaction它没有Buffer Pool WAL Background Thread Compaction原因不是缺少功能。而是设计哲学不同。LMDB认为操作系统已经做好 缓存 页管理 刷盘 虚拟内存数据库只需要管理数据结构第五部分C使用、批量写入与视觉行业应用第十九章 LMDB C基本使用LMDB提供的是C API。但是C项目中通常会进行一层封装。典型结构Application | LMDB Wrapper | LMDB C API | data.mdb19.1 创建数据库环境LMDB所有操作首先需要创建MDB_env它代表一个数据库环境。示例#includelmdb.h#includeiostreamMDB_env*envnullptr;intmain(){intrc;// 创建环境rcmdb_env_create(env);if(rc!0){std::coutcreate env failed;return-1;}// 设置最大数据库大小mdb_env_set_mapsize(env,10ULL*1024*1024*1024);// 打开数据库目录rcmdb_env_open(env,./lmdb_data,0,0664);if(rc!0){std::coutopen failed;return-1;}std::coutLMDB opened;mdb_env_close(env);}19.2 mapsize是什么意思LMDB使用mmap映射数据库文件。因此需要提前指定最大虚拟空间。例如mdb_env_set_mapsize(env,100GB);表示允许data.mdb 最大增长到100GB注意这不是立即分配100GB内存。只是虚拟地址空间。例如Virtual Memory 0GB | | 100GB真正使用只有写入数据部分。第二十章 写入数据LMDB所有写操作必须在Write Transaction中。流程Begin Transaction | Put | Commit20.1 插入Key-Value完整代码#includelmdb.h#includecstringvoidinsert(MDB_env*env){MDB_txn*txn;// 开启事务mdb_txn_begin(env,nullptr,0,txn);MDB_dbi dbi;// 打开默认数据库mdb_dbi_open(txn,nullptr,0,dbi);constchar*keyStrcamera001;constchar*valueStrimage_data;MDB_val key;key.mv_sizestrlen(keyStr);key.mv_data(void*)keyStr;MDB_val value;value.mv_sizestrlen(valueStr);value.mv_data(void*)valueStr;// 写入intrcmdb_put(txn,dbi,key,value,0);if(rc0){// 提交mdb_txn_commit(txn);}else{// 回滚mdb_txn_abort(txn);}}20.2 写入过程对应LMDB内部代码mdb_put()对应mdb_put ↓ Cursor定位 ↓ BTree查找 ↓ Copy-On-Write ↓ 创建Dirty Page ↓ Commit ↓ 更新Meta Page第二十一章 查询数据查询mdb_get()示例voidquery(MDB_env*env){MDB_txn*txn;mdb_txn_begin(env,nullptr,MDB_RDONLY,txn);MDB_dbi dbi;mdb_dbi_open(txn,nullptr,0,dbi);constchar*keyStrcamera001;MDB_val key;key.mv_sizestrlen(keyStr);key.mv_data(void*)keyStr;MDB_val value;intrcmdb_get(txn,dbi,key,value);if(rc0){std::coutvalue size:value.mv_size;}mdb_txn_abort(txn);}注意这里value.mv_data不是复制的数据。它直接指向mmap区域所以读取非常快。第二十二章 删除数据删除mdb_del()示例mdb_del(txn,dbi,key,nullptr);但是删除并不会立即删除Page。原因MVCC。流程Delete ↓ 旧Page保留 ↓ 加入Free List ↓ 未来复用第二十三章 Cursor遍历这是LMDB最强功能之一。23.1 创建CursorMDB_cursor*cursor;mdb_cursor_open(txn,dbi,cursor);23.2 从第一条开始MDB_val key;MDB_val value;intrc;rcmdb_cursor_get(cursor,key,value,MDB_FIRST);23.3 循环遍历while(rc0){std::coutkey size:key.mv_size;rcmdb_cursor_get(cursor,key,value,MDB_NEXT);}内部第一次Root ↓ Leaf1 ↓ key1下一次key2不会重新Root Search第二十四章 批量写入优化视觉行业非常重要。例如训练集100万张图片如果每张一个Transaction。错误方式image1 Commit image2 Commit image3 Commit速度非常慢。原因每次Flush Meta更新 fsync推荐方式批量Begin Transaction image1 image2 image3 ... image10000 Commit示例for(inti0;i10000;i){mdb_put(txn,dbi,key,value,0);}mdb_txn_commit(txn);性能提升可能几十倍。第二十五章 LMDB在视觉行业中的应用LMDB在AI领域非常常见。例如Caffe Deep Learning Framework 的训练数据格式LMDB Dataset25.1 为什么视觉行业喜欢LMDB视觉数据特点特点1大量小文件例如工业检测一天500万张图片文件img001.png img002.png img003.png ...普通文件系统问题目录查找 inode 文件打开关闭 随机IO开销巨大。LMDB变成dataset.mdb一个文件。读取key ↓ binary image25.2 图片存储例如Keycamera01_20260808_00001ValueJPEG Binary结构Leaf Page key | | Overflow Pages | | JPEG Data读取mdb_get()↓ 返回地址 ↓ cv::Mat例如cv::Matimg(height,width,CV_8UC3,value.mv_data);避免memcpy25.3 点云数据工业3D视觉例如Point Cloud 100万个点每个点structPoint{floatx;floaty;floatz;};大小12MB保存Key:scan_00001Value:binary point array读取Point*ptsstatic_castPoint*(value.mv_data);直接访问。25.4 Tensor训练数据深度学习Tensor例如float32 224*224*3大小600KB保存key: sample_001 value: Tensor binary训练DataLoader ↓ LMDB Cursor ↓ Tensor第二十六章 LMDB与NAS/对象存储结合实际工业系统不会只用LMDB。常见架构Camera | | NAS/Object Storage | | Dataset Builder | | LMDB | | Training例如原始数据NAS 100TB图片训练前转换LMDB Dataset 500GB优势训练读取速度提升。第二十七章 LMDB适合什么场景非常适合1. 读多写少例如AI训练写一次 读取百万次2. 大量小文件例如图片百万级3. 嵌入式数据库例如设备配置 历史数据 缓存不适合高频随机写例如订单系统每秒几十万update不适合。原因单Writer。超大规模分布式例如PB级数据。应该使用Apache CassandraApache HBaseTiKV第二十八章 LMDB核心总结LMDB本质不是一个传统数据库。它更像一个超级快的 持久化BTree核心mmap ↓ 直接访问文件 BTree ↓ 快速索引 Copy-On-Write ↓ 无WAL MVCC ↓ 读写隔离 Cursor ↓ 高速遍历LMDB完整架构图Application | | MDB API | | Transaction | ------------------------ | | Cursor MVCC | | BTree Snapshot | | -------- | | Branch Leaf | | Overflow Page | | mmap | | data.mdb至此LMDB核心原理全部介绍完成。