在SQLite里发起事务并不是简单地执行BEGIN就完事,紧跟其后的修饰符会直接决定数据库锁的获取时机与排斥范围。不少人在高并发写入时出现database is locked错误,根源往往就是没弄清楚DEFERRED、IMMEDIATE与EXCLUSIVE这三者的行为边界。本文从锁模型出发,把这三种事务类型的底层逻辑、使用场景和代码写法逐一拆开来讲。

SQLite锁机制与事务起点的关系
SQLite在文件级实现了一套轻量锁体系,包含UNLOCKED、SHARED、RESERVED、PENDING和EXCLUSIVE五种状态。普通读操作只需SHARED锁,写操作则要先拿RESERVED,再经PENDING最终升级为EXCLUSIVE。事务起始修饰符的作用,就是提前规定什么时候去碰这些锁。
DEFERRED是默认模式,执行BEGIN DEFERRED时数据库完全不加任何锁,直到第一条SELECT才加SHARED,第一条INSERT或UPDATE才尝试RESERVED。这种延迟加锁降低了启动开销,但如果两个事务都 defer 到写阶段才抢锁,就容易在升级时互相阻塞甚至死锁。IMMEDIATE在BEGIN IMMEDIATE那一刻就直接拿RESERVED锁,等于提前占住写名额,别的事务再想写就会立即失败或等待,但读仍可进行。EXCLUSIVE则一上来就霸占EXCLUSIVE锁,期间任何连接都不能读也不能写。
从内核角度看,锁的获取越靠前,冲突暴露得越早,程序也越容易用重试逻辑兜底。理解这张锁时序表,是选对事务类型的前提。下面用一段C伪代码展示不同起点下的锁状态变化:
// 假设db为已打开的sqlite3句柄 sqlite3_exec(db, "BEGIN DEFERRED", 0, 0, 0); // 此时无锁 sqlite3_exec(db, "SELECT * FROM t", ...); // 加SHARED锁 sqlite3_exec(db, "UPDATE t SET v=1", ...); // 尝试RESERVED->EXCLUSIVE sqlite3_exec(db, "BEGIN IMMEDIATE", 0, 0, 0); // 直接RESERVED锁 sqlite3_exec(db, "UPDATE t SET v=2", ...); // 后续可平滑升级 sqlite3_exec(db, "BEGIN EXCLUSIVE", 0, 0, 0); // 直接EXCLUSIVE锁 // 此时其他连接连SELECT都会被拒
三种事务类型的典型应用场景对比
选DEFERRED适合读多写少且写逻辑靠后的场景,比如先跑一堆查询确认数据存在,再决定要不要插入。因为它的锁是按需出现,短时间重叠写的概率低。但如果你明确知道接下来一定要写,还用DEFERRED,就等于把冲突留到执行中途,那时已经做了部分计算,回滚成本更高。
IMMEDIATE常见于“先占坑再慢慢改”的批处理。例如后台脚本要汇总多个表后回写,用BEGIN IMMEDIATE能在启动时就确保自己有写权,避免做到一半被别的进程插队导致database is locked。它不阻塞读,对线上读服务影响小,是多数写任务的安全默认。EXCLUSIVE则用于结构性变更,比如VACUUM、大量删表重建,或者测试里要绝对隔离。此时宁可让全库不可访问几秒,也不想被并发读拖慢写升级。
我们用一个简表归纳差异:
| 类型 | 起始加锁 | 排斥读 | 排斥写 | 适用情形 |
|---|---|---|---|---|
| DEFERRED | 无(延迟) | 否 | 写升级时 | 读后偶写 |
| IMMEDIATE | RESERVED | 否 | 是 | 明确写任务 |
| EXCLUSIVE | EXCLUSIVE | 是 | 是 | 维护/隔离 |
实际项目中,如果业务代码用ORM,往往默认走DEFERRED,需要显式写BEGIN IMMEDIATE才能改。忽视这层,就会在并发稍高时频繁重试。
代码写法与避坑实践
在SQLite命令行或编程接口里,修饰符紧跟BEGIN。写错顺序如BEGIN TRANSACTION IMMEDIATE也是合法的,但BEGIN DEFERRED可省略DEFERRED。Python的sqlite3模块默认不暴露修饰符,需要靠execute("BEGIN IMMEDIATE")手动发指令,而不是用默认commit前的隐式事务。
一个常见坑是:在DEFERRED事务里先读后写,另一个连接也这么干,两者都拿着SHARED,然后都要升EXCLUSIVE,谁也不让谁,SQLite会检测死锁并让其中一个失败。此时应统一改成IMMEDIATE,让先到者占RESERVED,后到者立刻知道没戏,重试即可。下面给出Python正确写法:
import sqlite3
conn = sqlite3.connect("app.db")
try:
# 显式使用IMMEDIATE避免延迟写锁冲突
conn.execute("BEGIN IMMEDIATE")
row = conn.execute("SELECT cnt FROM counter WHERE id=1").fetchone()
new_cnt = row[0] + 1
conn.execute("UPDATE counter SET cnt=? WHERE id=1", (new_cnt,))
conn.execute("COMMIT")
except sqlite3.OperationalError as e:
conn.execute("ROLLBACK")
print("事务冲突,可重试:", e)
另外注意,EXCLUSIVE在WAL模式下行为略有不同,它仍允许其他连接读最近提交版本,但禁止新写。若你用PRAGMA journal_mode=WAL,别误以为EXCLUSIVE能完全冻结节点的读。掌握这些细节,才能在延迟与隔离之间调到合适平衡点。