SQLite WAL 模式:先把“写锁”这件事讲清楚
SQLite WAL 模式:先把“写锁”这件事讲清楚
用 SQLite 做单机应用时,最常踩的坑不是 SQL 写错,而是并发一上来就报 database is locked。默认的回滚日志(rollback journal)模式下,写事务会独占整个数据库文件,读操作也得排队等。换成 WAL(Write-Ahead Logging)模式后,读和写可以并行,这个坑基本就平了。
WAL 到底改了什么
开启 WAL 很简单,一条命令:
PRAGMA journal_mode=WAL;
开启后,写操作不再直接改数据库文件,而是先追加写到 -wal 文件里;读操作读的是数据库文件加 WAL 里已提交的内容。于是出现两个关键变化:第一,读不再阻塞写,写也不再阻塞读;第二,写事务之间仍然是串行的——WAL 解决的是读写冲突,不是写写冲突。
检查点(checkpoint)是 WAL 的另一个核心概念。当 WAL 文件积累到一定大小,SQLite 会把已提交的数据合并回主数据库文件,这个过程叫 checkpoint。默认是被动触发的,应用基本不用管,但要知道一件事:WAL 文件和主文件必须放在同一个目录,且 -shm 共享内存文件也要能正常创建,否则 WAL 会退化甚至报错。
三个实操要点
第一,连接要复用,不要每次操作都开关库。 频繁开关连接会让 checkpoint 和锁状态变得不可预测,长连接加连接池才是正解。Python 里可以用一个模块级连接配合线程锁,或者直接用 sqlite3 的 check_same_thread=False 加显式锁。
第二,写事务要短。 WAL 下写事务仍会互斥,一个大事务拖几秒,所有写请求都会排队。正确做法是把一次写操作拆成“准备数据”和“快速提交”两步,事务里只放真正的写语句:
import sqlite3
db = sqlite3.connect("app.db", timeout=10)
db.execute("PRAGMA journal_mode=WAL;")
def save_record(record):
# 先在事务外算好要写的数据,事务里只做一次写入
db.execute("BEGIN IMMEDIATE") # 立刻拿写锁,失败会等 timeout 而不是马上报错
try:
db.execute("INSERT INTO records (name, value) VALUES (?, ?)", record)
db.commit()
except Exception:
db.rollback()
raise
注意 timeout=10 这个参数:它不是重试业务逻辑,而是让 SQLite 在拿不到锁时等 10 秒,短暂的锁竞争就这样被消化掉了。很多 database is locked 报错,其实把 timeout 从默认 5 秒调大、再把事务缩短就解决了。
第三,备份要按 WAL 的规矩来。 直接复制数据库文件可能丢掉 WAL 里还没 checkpoint 的数据。正确做法是用 SQLite 自带的备份 API,或者先执行 PRAGMA wal_checkpoint(TRUNCATE) 再复制文件。sqlite3 模块里有 Connection.backup(),在线备份不断服务也能用。
WAL 不是万能药
WAL 也有边界:它不适合网络文件系统(NFS 上共享内存文件不可靠),也不适合“多进程同时写、写冲突频繁”的场景——写写串行这个本质没变。如果写并发真的很高,该上 PostgreSQL 还是得上。但对绝大多数单机工具、桌面应用和嵌入式场景,WAL 模式加短事务加合理 timeout,已经足够把 SQLite 的并发表现从“勉强能用”推到“稳稳够用”。
- 点赞
- 收藏
- 关注作者
评论(0)