LMDB (Lightning Memory-Mapped Database) 是一个嵌入式、事务性的 键-值存储 (Key-Value Store) 数据库库,它以其高性能、零拷贝 (Zero-Copy) 和高可靠性而闻名。下面介绍 LMDB 的文件格式和主要特性。
LMDB 文件格式概述
LMDB 通常使用两个文件来持久化数据和管理并发访问:
-
data.mdb:
- 这是主要的数据文件,包含了所有的键-值数据。
- 它的结构是基于 B+ 树 (B+ tree) 的。
- 整个文件被内存映射 (Memory-Mapped) 到进程的地址空间,这是其高性能的关键,数据读取可以直接通过内存访问,无需额外的系统调用或数据拷贝(零拷贝)。
- 文件内部被组织成固定大小的页 (Pages),通常大小与操作系统的内存页大小(例如 4KB)一致。
-
lock.mdb:
- 这是一个锁文件,用于在多个进程或线程之间协调并发访问,特别是写事务。
- 它包含元数据和锁定信息。
LMDB 的核心特性
内存映射 (Memory-Mapped)
- 高效访问: 数据库文件被直接映射到内存。读取数据时,直接从映射的内存中返回数据,避免了磁盘 I/O 和内存之间的
malloc/memcpy 操作,从而实现极高的读取性能。
B+ 树结构
- 所有键-值对都存储在一个或多个 B+ 树中,提供了范围查询 (Range-based search) 的能力,并且键是按序存储的(有序映射)。
事务和 ACID 保证 (Transaction and ACID)
- LMDB 提供了完全的事务性,具有 ACID(原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability)特性。
- 写操作是序列化的,同一时间只允许一个写事务处于活跃状态。
多版本并发控制 (MVCC)
- LMDB 使用 MVCC 机制:
- 读事务是无锁的,并且永远不会阻塞写事务,写事务也不会阻塞读事务。
- 读者看到的是数据库的一致性快照。读取性能可以随着 CPU 数量的增加而线性扩展。
写时复制 (Copy-on-Write)
- 进行数据修改时,LMDB 采用写时复制 (Copy-on-Write) 策略,它永远不会覆盖正在使用的数据页。
- 这种设计保证了磁盘上的结构始终有效,即使系统或应用崩溃,数据库也不会处于损坏状态,无需特殊恢复过程。
可移植性限制
- 重要提示: LMDB 的文件格式是依赖于体系结构的(例如 32 位/64 位、字节序)。因此,将数据库从一台机器迁移到另一台不同架构的机器时,通常需要先进行导出/导入操作。
示例
以下是一个完整的 Python 示例,展示了如何使用 lmdb 库进行创建、读取、修改等基本操作。
pip install lmdb
import lmdb
import os
# --- 1. 环境准备与初始化 ---
# 定义数据库存储的目录
db_path = './my_lmdb'
# 确保目录存在
os.makedirs(db_path, exist_ok=True)
# 打开或创建 LMDB 环境
# map_size 指定了数据库的最大大小,根据需要调整
env = lmdb.open(db_path, map_size=1024*1024*10) # 10 MB
print(f"数据库已打开/创建,路径: {db_path}")
# --- 2. 写入数据 (Put) ---
print("\n--- 写入数据 ---")
# 使用 write 事务来修改数据库
with env.begin(write=True) as txn:
# put(key, value) 将键值对存入数据库
# 注意:key 和 value 都必须是 bytes 类型
txn.put(b'key1', b'value1')
txn.put(b'key2', b'value2')
txn.put(b'key3', b'value3')
print("已写入 3 条记录: key1->value1, key2->value2, key3->value3")
# --- 3. 读取数据 (Get) ---
print("\n--- 读取数据 ---")
# 使用 read 事务来读取数据库
with env.begin() as txn:
# get(key) 获取指定 key 的 value
value = txn.get(b'key2')
print(f"读取 'key2': {value}")
# 尝试获取一个不存在的 key
non_existent_value = txn.get(b'key_does_not_exist')
print(f"读取 'key_does_not_exist': {non_existent_value}")
# --- 4. 修改数据 ---
print("\n--- 修改数据 ---")
# 修改数据与写入数据使用相同的方法 put()
with env.begin(write=True) as txn:
# 如果 key 存在,put 会覆盖旧的 value
txn.put(b'key2', b'new_value_for_key2')
print("已修改 'key2' 的值为 'new_value_for_key2'")
# 验证修改
with env.begin() as txn:
new_value = txn.get(b'key2')
print(f"再次读取 'key2' 验证修改: {new_value}")
# --- 5. 遍历数据 (Iterate) ---
print("\n--- 遍历所有数据 ---")
# 使用 cursor 来遍历数据库中的所有键值对
with env.begin() as txn:
cursor = txn.cursor()
for key, value in cursor:
print(f" 键: {key}, 值: {value}")
# --- 6. 删除数据 (Delete) ---
print("\n--- 删除数据 ---")
with env.begin(write=True) as txn:
# delete(key) 删除指定的 key-value 对
deleted = txn.delete(b'key1')
if deleted:
print("成功删除 'key1'")
else:
print("'key1' 不存在,无法删除")
# 验证删除
with env.begin() as txn:
remaining_value = txn.get(b'key1')
print(f"尝试读取已删除的 'key1': {remaining_value}")
# --- 7. 统计信息 ---
print("\n--- 数据库统计信息 ---")
with env.begin() as txn:
stat = env.stat()
print(f"数据库大小 (条目数): {stat['entries']}")
# --- 8. 清理与关闭 ---
print("\n--- 清理与关闭 ---")
# 关闭环境以释放资源
env.close()
print("数据库环境已关闭。")
# (可选) 删除数据库文件夹以清理
# import shutil
# shutil.rmtree(db_path)
# print(f"临时数据库 '{db_path}' 已被删除。")
代码解释
import lmdb 和 os: 导入 lmdb 库进行数据库操作,导入 os 库用于处理文件系统路径。
db_path 和 os.makedirs: 定义一个本地文件夹作为数据库存储位置,并使用 os.makedirs 确保该文件夹存在。
lmdb.open: 这是打开或创建 LMDB 数据库环境的关键函数。map_size 参数非常重要,它定义了数据库文件能增长到的最大字节数。如果数据量超过此大小,写入操作会失败。
env.begin(write=True): 开始一个写事务。任何修改数据库(put, delete)的操作都必须在写事务中进行。使用 with 语句可以确保事务在代码块结束时自动提交或回滚,即使发生异常也能保证资源被正确释放。
txn.put(key, value): 将一个键值对存入数据库。注意:key 和 value 都必须是 bytes 类型,不能是 str。如果需要存储字符串,需要用 .encode() 方法转换。
env.begin() (无 write=True): 开始一个只读事务。用于读取数据(get, cursor 遍历)。
txn.get(key): 根据 key 获取对应的 value。如果 key 不存在,则返回 None。
txn.cursor(): 创建一个游标对象,用于高效地遍历数据库中的所有键值对。
txn.delete(key): 删除指定的 key 及其对应的 value。如果 key 存在,返回 True;如果不存在,返回 False。
env.stat(): 获取数据库的统计信息,如条目总数。
env.close(): 在程序结束前,务必调用 env.close() 来关闭数据库环境,释放所有相关资源。