《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》-第三篇:LMDB 深度解析二

文章目录

  • [《现代 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核心原理全部介绍完成。

相关推荐
提笔了无痕1 小时前
Agent 上下文管理详解、Context设计与构建
数据库·oracle·agent·context
用户7152287381411 小时前
数据库字段冗余,真的只是用空间换时间吗?
数据库
沉下去,苦磨练!1 小时前
Human‑in‑the‑Loop
数据库·oracle
吠品1 小时前
Java中实现拓扑排序的两种方式:Kahn算法和DFS
java·数据库·notepad++
轻揉小乔 真新人1 小时前
数据库架构的升级和变更
数据库·数据库架构
禁默2 小时前
信创实战:Python + SQLAlchemy 接入金仓数据库(从驱动安装到完整 CRUD)
开发语言·数据库·python
范什么特西2 小时前
知识总结04(redis)
数据库·redis·缓存
小黑技术栈2 小时前
web前端基础到入门——14day
前端·数据库·oracle
福大大架构师每日一题3 小时前
agno v2.8.7 发布:顾问模型、精准路线、调度能力全面升级,10项关键修复一次看懂
java·开发语言·数据库