不用现成sqlite3或shelve是因为仅三类场景需自实现b+树持久化:精确控制页布局、教学理解写盘逻辑、对接特定二进制格式;其余情况sqlite3更健壮且支持事务并发。

为什么不用现成的 sqlite3 或 shelve?
直接用 sqlite3 或 shelve 做键值存储,99% 的场景已经够用。自己实现 B+ 树持久化,通常只在三类情况:需要精确控制页大小和磁盘布局(比如嵌入式日志索引)、教学理解 B+ 树写盘逻辑、或对接特定二进制格式(如 WAL + 页面缓存)。否则,sqlite3 的 B+ 树引擎更健壮,且支持事务和并发。
struct 打包页结构时字节序和对齐必须显式指定
B+ 树持久化核心是把内存节点序列化为固定长度页写入文件,struct 是最轻量的选择,但默认行为容易踩坑:
-
struct.pack('i', x)用的是本机字节序,跨平台读写会错;必须用'<i>(小端)或 <code>'>i'(大端)显式声明 - 结构体字段之间可能有填充字节,比如
'iH'在 x86_64 上实际占 8 字节(因H对齐到 2 字节边界);应加'='表示标准尺寸且无填充:'=iH' - 字符串字段不能直接 pack,需先
encode('utf-8')并截断或补零,例如key.encode('utf-8')[:255].ljust(256, b'\0')
页内键值偏移必须用绝对位置而非相对偏移
很多初学者尝试在页头存一个“键偏移数组”,结果发现插入新键后所有偏移都要重算,性能崩坏。正确做法是让每个键值对占据固定槽位:
- 定义页大小为 4096 字节,其中前 8 字节存元信息(页类型、键数量、父页号等)
- 剩余空间划分为 N 个固定长槽位,例如每个槽位 64 字节:8 字节键长 + 24 字节键内容 + 4 字节值偏移 + 28 字节预留
- 这样任意键可通过
base_offset + slot_idx * SLOT_SIZE直接定位,插入只需找空槽,无需移动其他数据
这种设计牺牲部分空间利用率,换来 O(1) 随机访问 —— 对磁盘 IO 密集型操作更重要。
刷盘时必须 os.fsync() 而非仅 file.flush()
内存缓冲区不等于磁盘数据。只调 file.flush() 只保证数据进 OS 缓冲区,断电仍丢数据。B+ 树一致性依赖写顺序(如先写子页再更新父页指针),所以:
- 每次修改页后,必须
file.seek(page_offset); file.write(page_bytes); file.flush(); os.fsync(file.fileno()) - 避免用
with open(...) as f:自动关闭——关闭时 flush 不保证 fsync,得手动补 - 若频繁更新,可批量写页 + 单次 fsync,但需额外维护已写页列表,复杂度上升
真正难的不是树逻辑,而是把“页原子写入”和“父子页依赖顺序”在 crash 后还能恢复 —— 这需要 WAL 或影子页机制,超出单文件 B+ 树简易实现范围。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











