← 中文资源

简体中文版本

node-sqlite3 SQLITE_BUSY — 数据库被锁定

通过启用 WAL 模式、设置 busy timeout 并缩短写事务,修复 node-sqlite3 间歇性的 SQLITE_BUSY database is locked。

阅读英文原文

症状

本篇调试指南针对“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 非零。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。