SQLite作为轻量级嵌入式数据库,在桌面软件、移动端和小型服务中应用广泛。但当多个线程或进程同时读写同一个数据库文件时,开发者往往会遇到写入失败、读取阻塞甚至数据损坏等并发问题。理解SQLite内部的锁机制和可用的隔离模式,是写出健壮数据层的前提。

SQLite锁机制与并发模型
SQLite在文件层面上实现了多级锁来控制并发访问。最基础的是共享锁(SHARED),多个读连接可以同时持有共享锁进行查询;当某个连接需要写入时,必须先获取保留锁(RESERVED),再升级为写锁(PENDING)和排他锁(EXCLUSIVE)。在传统的回滚日志(ROLLBACK JOURNAL)模式下,只要有写者持有排他锁,其他所有读写操作都会被阻塞,这正是“数据库被锁定”错误的根源。
从实现细节来看,SQLite的锁是粗粒度的文件锁,并非行级锁。这意味着即便两个线程修改的是不同表的不同行,只要它们位于同一数据库文件,写操作仍然互斥。因此高并发写入场景并不适合原生SQLite。但在“单写多读”模型中,SQLite表现良好:一个后台线程负责写,多个前台线程负责读,彼此干扰极小。
为了缓解锁冲突,SQLite提供了busy_timeout参数。设置该参数后,当连接遇到SQLITE_BUSY时不会立即报错,而是在指定毫秒内重试获取锁。例如设置五千毫秒,可以让短暂冲突自行消化。不过超时不能解决根本的写并发瓶颈,只是降低错误率。以下代码展示如何在Python中设置忙等待:
import sqlite3
conn = sqlite3.connect('app.db')
conn.execute('PRAGMA busy_timeout = 5000')
# 此后写操作遇到锁会重试最多5秒
conn.execute('INSERT INTO logs(msg) VALUES (?)', ('hello',))
conn.commit()
conn.close()
WAL模式如何提升并发能力
预写日志(WAL)模式是SQLite改善并发的关键特性。在WAL模式下,写操作不再直接修改主数据库文件,而是追加到单独的-wal文件中,并定期通过检查点合并回主库。这种机制使得读操作可以继续读取旧版本数据,而写操作在另一个文件中推进,从而实现读写互不阻塞。
需要明确的是,WAL模式只解除了“读阻塞写”和“写阻塞读”的限制,并没有解除“写阻塞写”。任意时刻仍只能有一个写者。对于典型应用,这已经足够:界面线程频繁读,工作线程偶尔写,用户几乎感知不到延迟。开启WAL只需执行PRAGMA指令,且对既有数据库兼容:
PRAGMA journal_mode = WAL; -- 查看当前模式 PRAGMA journal_mode;
WAL也带来运维差异。由于存在-wal和-shm文件,备份时需要一并处理,否则可能丢失未检查点数据。另外在部分网络文件系统上,WAL的共享内存机制可能失效,此时应退回默认模式。下表对比两种模式的核心差异:
| 特性 | 回滚日志模式 | WAL模式 |
|---|---|---|
| 读写并发 | 写阻塞读 | 读写不阻塞 |
| 写写并发 | 互斥 | 互斥 |
| 崩溃恢复 | 依赖日志回滚 | 重放wal |
| 适用文件系统 | 通用 | 本地磁盘最佳 |
工程实践中的并发控制方案
在真实项目中,推荐将写操作集中到单一线程或使用队列串行化。例如用Python的queue模块把全部写请求压入一个专属消费者线程,其他线程只做查询。这样从架构上规避了多写冲突,也简化了事务管理。读连接则可以按线程各自创建,配合WAL模式实现高吞吐查询。
另一个常见做法是使用连接池并正确设置隔离级别。SQLite的默认隔离是SERIALIZABLE,但因其锁机制,实际表现为读已提交偏多。在Node.js中可借助better-sqlite3的同步API避免异步重入导致的锁混乱。下方示例展示用互斥锁保护写函数的Node风格代码:
const Database = require('better-sqlite3');
const db = new Database('app.db');
db.pragma('journal_mode = WAL');
function safeWrite(sql, params) {
const tx = db.transaction((s, p) => {
db.prepare(s).run(p);
});
tx(sql, params);
}
safeWrite('INSERT INTO t(v) VALUES (?)', ['x']);
最后要警惕长事务。无论是读还是写,事务持有锁的时间越长,冲突概率越高。应将批量写入拆为小批次并提交,读事务尽快释放。配合PRAGMA synchronous = NORMAL在WAL下可兼顾安全与性能。当遵循单写多读、启用WAL、控制事务粒度这三条原则时,SQLite完全能支撑中小规模并发场景。