检查点记录一部分已经完成的工作,使数据库恢复不必把整个日志历史都视为待处理内容。具体操作取决于引擎:PostgreSQL 同步检查点要求的数据变更,建立重做起点;SQLite 的 WAL 模式则把符合条件的已提交页面帧从日志转移到主数据库文件。两者都不能简单当作事务提交的另一个名字。
坏掉的样子 已经请求检查点与检查点完整完成,是两个不同观察。SQLite 读者可能持有旧快照,阻止较新的日志帧写入主文件,或者阻止日志重置。被动检查点可以只做当前能够完成的部分,然后正常返回。若只看成功标志,不看日志总帧数与已完成帧数,就会漏掉尚未处理的工作,以及那个仍然阻止完成的读事务。
日志大小也要放在具体上下文中理解。文件可以继续复用原有空间,因此已分配大小不变时,检查点进度仍可能推进。PostgreSQL 完成检查点之后,也可能继续为归档或复制槽保留旧日志。为了让磁盘曲线变小而手动删除日志,会破坏恢复材料。应先核对引擎进度和保留义务,再判断文件大小究竟代表什么。
检查点间隔不是恢复时间的硬上限。重放速度、存储吞吐、写入形态以及实际崩溃位置,都会影响恢复耗时。更频繁的检查点可能减少未来重做,也可能增加平时写流量。把日志放到另一块设备,不会把所有数据页写入也搬过去,更不会使检查点的写盘成本自动消失。参数选择需要对应具体工作负载。
快速验证 把下面内容保存为 checkpoint_lab.py,使用普通的 python3 checkpoint_lab.py 运行,保持断言开启。脚本只使用标准库 SQLite 模块与私有临时文件。本次记录来自 Python 3.14.4 搭配 SQLite 3.53.0。比较结果时应同时查看运行时的引擎版本,因为 Python 版本与实际链接的 SQLite 版本并不是一回事。
import sqlite3 import tempfile from pathlib import Path with tempfile.TemporaryDirectory(prefix="xbcatm-checkpoint-") as directory: path = Path(directory) / "test.db" writer = sqlite3.connect(path, isolation_level=None, timeout=0.1) reader = sqlite3.connect(path, isolation_level=None, timeout=0.1) try: assert writer.execute("PRAGMA journal_mode=WAL").fetchone() == ("wal",) writer.execute("PRAGMA synchronous=FULL") writer.execute("PRAGMA wal_autocheckpoint=0") writer.execute("CREATE TABLE marker (id INTEGER PRIMARY KEY, value INTEGER)") writer.execute("INSERT INTO marker VALUES (1,10)") assert writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() == (0, 0, 0) reader.execute("BEGIN") assert reader.execute("SELECT value FROM marker").fetchone() == (10,) writer.execute("UPDATE marker SET value=20 WHERE id=1") assert writer.execute("SELECT value FROM marker").fetchone() == (20,) passive = writer.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone() blocked = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() print("passive", passive) print("truncate_with_reader", blocked) assert passive[0] == 0 and passive[1] > passive[2] >= 0 assert blocked[0] == 1 and blocked[1] > blocked[2] assert Path(str(path) + "-wal").stat().st_size > 0 assert reader.execute("SELECT value FROM marker").fetchone() == (10,) reader.execute("COMMIT") completed = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() print("truncate_after_reader", completed) assert completed == (0, 0, 0) assert Path(str(path) + "-wal").stat().st_size == 0 assert reader.execute("SELECT value FROM marker").fetchone() == (20,) print("fresh_reader", 20) finally: reader.close() writer.close() 初始截断检查点把值十写入主文件,并重置日志。随后一个读者显式开启事务,读取十;另一个连接在关闭自动检查点之后,把值二十提交。写者立刻就能读到二十,而旧读者仍然需要自己的原始快照。这个顺序用真实连接建立约束,不依赖线程恰好在某个微小时间窗口发生竞争。
...