LMDB 键值数据库介绍
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- example_lmdb.py
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()来关闭数据库环境,释放所有相关资源。
最近更新
最新评论