SQLite写入并发的三个坑
从锁粒度、WAL与连接管理三个角度,整理小项目里最容易遇到的SQLite写入失败,以及对应的排查顺序。
SQLite在小服务里非常好用,直到某天开始出现database is locked。这类问题通常不是SQLite不行,而是默认配置和实际写入模式不匹配。
坑一:把”轻量”理解成”可以随便并发写”
SQLite的写锁是库级的,同一时刻只允许一个写入者。多个请求同时写,后来的必须等busy_timeout,超时就抛错。这和行级锁的直觉完全不同。
排查顺序:
- 先看有没有长事务包裹了耗时逻辑(网络请求、循环处理写进了事务里);
- 再看写入是否集中在同一张表但分散在多个连接;
- 最后才考虑调参数。
把事务缩到最小,往往比调任何配置都有效。
# 反例:事务里做了不该做的事
with conn:
row = conn.execute("SELECT ...").fetchone()
result = call_remote_api(row) # 网络耗时占着写锁
conn.execute("UPDATE ...", (result,))
# 正解:先算完,再进事务
row = conn.execute("SELECT ...").fetchone()
result = call_remote_api(row)
with conn:
conn.execute("UPDATE ...", (result,))
坑二:WAL不是万能开关
打开WAL能让读写不互相阻塞,但它有前提:底层文件系统必须支持可靠的共享内存与文件锁。
- 网络文件系统、某些容器挂载卷、部分同步盘上,WAL可能不可用或直接报错;
- WAL会带来
-wal和-shm文件,备份时不能只拷主库文件; - 单机多进程且共享目录权限复杂时,启用前的验证成本比收益更高。
如果环境不支持WAL,老实的做法是串行化写入:在应用层用一个队列把写操作排队,单连接单写入者,问题立刻消失。对个人博客的评论量级,这完全够用。
// 用Promise链把写入串行化,避免并发写文件
let queue = Promise.resolve();
function enqueue(task) {
queue = queue.then(task, task);
return queue;
}
坑三:连接生命周期混乱
常见问题包括:每次请求新建连接却从不关闭、在多个模块里各自持有长连接、检查点上忘记PRAGMA busy_timeout。
建议固定下来:
- 一个进程内共享一个写连接,读连接按需;
- 显式设置
busy_timeout(例如5000毫秒),别依赖默认值0; - 用上下文管理器保证异常时连接归还;
- 所有写入走同一个入口,方便统一加重试与日志。
什么时候该换数据库
当出现下面任一信号,就该考虑PostgreSQL而不是继续调SQLite了:多机部署、写入QPS持续上百、需要跨服务事务、需要在线迁移与细粒度权限。
在那之前,单写入者 + 短事务 + 明确超时这三条,能解决绝大多数小项目的写入报错。
评论 0
评论保存在本站服务器,无需登录。带「作者」标识的是站主回复,请友善交流。
还没有评论,来写第一条。