症状
本篇调试指南针对“node-sqlite3 SQLITE_BUSY — 数据库被锁定”。内容涵盖可观察的症状、可复现的诊断路径、根本原因、具体修复方式,以及如何验证修复并防止回归。
可观察到的症状包括:
- 写入间歇性失败并报 “SQLITE_BUSY: database is locked”
- 并发写入进行时读取仍成功
- 仅在负载升高或多进程访问时才出现
诊断
先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。
步骤 1
db.run('PRAGMA busy_timeout = 5000')
设置超时后,短暂的锁竞争会等待并成功,而非立即抛错。
步骤 2
db.run('PRAGMA journal_mode = WAL')
WAL 允许写者活动时读者继续读,消除大多数读写竞争。
步骤 3
db.each('PRAGMA lock_status', (e, r) => console.log(e, r))
跨 await 或网络调用仍保持开启的写事务会长期持锁,饿死其他操作。
常见诱因(按出现频率排序):
- SQLite 同时只允许一个写者,另一写事务仍持有锁
- 未设置 busy timeout,锁冲突会立即失败而非等待
- 长写事务(常在内部含 await)长时间持有锁
根因
SQLite 在数据库文件级别串行化写入者。默认回滚日志模式下写者会阻塞读者与其他写者;未设 busy timeout 时任何冲突都会立即以 SQLITE_BUSY 报告。
修复
开启 WAL,设置数秒的 busy timeout,保持写事务短小且不包含 await,并通过单连接或应用级互斥锁串行化写入。
注意:WAL 会生成 -wal 与 -shm 伴生文件;备份时须一并处理,否则恢复可能丢数据。
import sqlite3 from 'sqlite3';
const db = new sqlite3.Database('./app.db');
db.serialize(() => {
db.run('PRAGMA journal_mode = WAL');
db.run('PRAGMA busy_timeout = 5000');
db.run('PRAGMA synchronous = NORMAL');
});
// Keep writes short; avoid awaits inside a transaction.
db.run('BEGIN IMMEDIATE');
db.run('UPDATE counters SET n = n + 1 WHERE id = ?', [1]);
db.run('COMMIT');
验证
在并发下运行写入负载并确认无 SQLITE_BUSY;检查 PRAGMA 输出中 journal_mode 为 wal 且 busy_timeout 非零。
修复后补充一条回归测试,复现原始症状,防止问题再次出现。
这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。