文章目录

《现代 Key-Value 数据库原理:从 B+Tree 到 LSM Tree》

第三篇:LMDB 深度解析

第四部分:Cursor机制与LMDB源码结构分析


第十三章 LMDB Cursor深入解析

在很多Key-Value数据库中:

查询:

key → value

通常是主要操作。

但是在工业视觉、AI训练等场景:

经常需要:

遍历全部数据

范围查询

顺序读取

批量处理

例如:

训练集:

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:

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

数据库环境

结构:

MDB_env

对应:

一个LMDB数据库实例

例如:

mdb_env_create(&env);

mdb_env_open(
    env,
    "./database",
    0,
    0664
);


内部保存:

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流程源码分析

调用:

mdb_get()

例如:

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地址


例如:

MDB_val data;


data.mv_data

实际:

指向:

data.mdb映射区域

第十六章 LMDB一次Put流程分析

写入:

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++项目中通常会进行一层封装。

典型结构:

Application

    |

LMDB Wrapper

    |

LMDB C API

    |

data.mdb


19.1 创建数据库环境

LMDB所有操作首先需要创建:

MDB_env

它代表:

一个数据库环境。

示例:

#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使用:

mmap

映射数据库文件。

因此需要提前指定:

最大虚拟空间。

例如:

mdb_env_set_mapsize(
    env,
    100GB
);

表示:

允许:

data.mdb

最大增长到100GB

注意:

这不是立即分配:

100GB内存。

只是:

虚拟地址空间。

例如:

Virtual Memory


0GB

 |

 |

100GB


真正使用:

只有写入数据部分。


第二十章 写入数据

LMDB所有写操作必须在:

Write Transaction

中。

流程:

Begin Transaction

        |

      Put

        |

     Commit


20.1 插入Key-Value

完整代码:

#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内部

代码:

mdb_put()

对应:

mdb_put

↓

Cursor定位

↓

B+Tree查找

↓

Copy-On-Write

↓

创建Dirty Page

↓

Commit

↓

更新Meta Page


第二十一章 查询数据

查询:

mdb_get()

示例:

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);

}


注意:

这里:

value.mv_data

不是复制的数据。

它直接指向:

mmap区域

所以:

读取非常快。


第二十二章 删除数据

删除:

mdb_del()

示例:

mdb_del(
    txn,
    dbi,
    &key,
    nullptr
);


但是:

删除并不会立即删除Page。

原因:

MVCC。

流程:

Delete


↓

旧Page保留


↓

加入Free List


↓

未来复用


第二十三章 Cursor遍历

这是LMDB最强功能之一。


23.1 创建Cursor

MDB_cursor* cursor;


mdb_cursor_open(
    txn,
    dbi,
    &cursor
);


23.2 从第一条开始

MDB_val key;
MDB_val value;


int rc;


rc =
mdb_cursor_get(
    cursor,
    &key,
    &value,
    MDB_FIRST
);


23.3 循环遍历

while(rc == 0)
{

    std::cout
        << "key size:"
        << key.mv_size;


    rc =
    mdb_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(int i=0;i<10000;i++)
{

    mdb_put(
        txn,
        dbi,
        &key,
        &value,
        0
    );

}


mdb_txn_commit(txn);


性能提升:

可能:

几十倍。


第二十五章 LMDB在视觉行业中的应用

LMDB在AI领域非常常见。

例如:

Caffe Deep Learning Framework 的训练数据格式:

LMDB Dataset

25.1 为什么视觉行业喜欢LMDB?

视觉数据特点:

特点1:大量小文件

例如:

工业检测:

一天:

500万张图片

文件:

img001.png

img002.png

img003.png

...


普通文件系统:

问题:

目录查找

inode

文件打开关闭

随机IO

开销巨大。


LMDB:

变成:

dataset.mdb

一个文件。

读取:

key

↓

binary image


25.2 图片存储

例如:

Key:

camera01_20260808_00001

Value:

JPEG Binary

结构:

Leaf Page


key


|

|

Overflow Pages


|

|

JPEG Data


读取:

mdb_get()

↓

返回地址

↓

cv::Mat


例如:

cv::Mat img(
    height,
    width,
    CV_8UC3,
    value.mv_data
);

避免:

memcpy


25.3 点云数据

工业3D视觉:

例如:

Point Cloud

100万个点

每个点:

struct Point
{
    float x;
    float y;
    float z;
};

大小:

12MB

保存:

Key:

scan_00001

Value:

binary point array


读取:

Point* pts =
static_cast<Point*>(
    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 Cassandra
  • Apache HBase
  • TiKV

第二十八章 LMDB核心总结

LMDB本质:

不是一个传统数据库。

它更像:

一个超级快的

持久化B+Tree

核心:

mmap

↓

直接访问文件


B+Tree

↓

快速索引


Copy-On-Write

↓

无WAL


MVCC

↓

读写隔离


Cursor

↓

高速遍历


LMDB完整架构图

                 Application


                       |

                       |


                 MDB API


                       |

                       |


                 Transaction


                       |

          +------------+------------+

          |                         |

       Cursor                    MVCC


          |                         |


       B+Tree                 Snapshot


          |

          |

     +----+----+

     |         |

 Branch     Leaf


               |

               |

           Overflow Page



               |

               |

            mmap


               |

               |

           data.mdb


至此,LMDB核心原理全部介绍完成。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐