bigywater.tech
常见报错 · 55

SQLite database is locked:读后写的延迟事务升级锁时 timeout=10 也 0.0 ms 立刻失败,WAL 模式 8 线程读后写失败 1372/1600、BEGIN IMMEDIATE 为 0

“database is locked” 的常见建议是“调大 timeout”或“开 WAL”。这篇用 Python 3.15 自带的 SQLite 3.54.1 在本机逐个验证:到底是谁挡了谁、timeout 什么时候不起作用、忘了 commit 的后果,以及 8 个线程同时读后写时各种配置的真实失败数。

0.1 ms
锁升级时第二个连接失败的耗时,即使 timeout=10 秒
1372 / 1600
8 线程读后写,WAL + 默认 BEGIN 的失败次数;换成 BEGIN IMMEDIATE 为 0
3.06 s
timeout=5 时 B 等到 A 在 3 秒后提交并成功的等待时间

写-写冲突

实测 · 本机执行输出
  B 的 timeout=0:失败 OperationalError: database is locked,等了 0.00 s
  B 的 timeout=1:失败 OperationalError: database is locked,等了 1.06 s
  B 的 timeout=5:成功,等了 3.06 s

另一个连接持有写锁时,等待时间取决于 timeout。

谁挡谁

实测 · 本机执行输出
  写者持有未提交写时,读者 SELECT:  DELETE 模式 -> ok                                                           | WAL 模式 -> ok
  读者游标没读完时,写者 INSERT(自动提交):  DELETE 模式 -> OperationalError: database is locked                         | WAL 模式 -> ok
  两个写者:A 持有写锁,B INSERT:  DELETE 模式 -> OperationalError: database is locked                         | WAL 模式 -> OperationalError: database is locked

WAL 让读写互不阻塞,但同一时刻仍只有一个写者。

锁升级

实测 · 本机执行输出
  BEGIN(DEFERRED)+SELECT 之后,timeout=10 秒:B 的 INSERT -> OperationalError: database is locked,用时 0.1 ms;A 的 INSERT -> ok,用时 0.00 s
  改用 BEGIN IMMEDIATE(A 持有 0.5 s):B 的 BEGIN IMMEDIATE 排队等了 0.57 s(timeout=10 的保护有效),之后 SELECT 和 INSERT 都成功,两行都写进去了:2 行

先读后写的延迟事务升级写锁失败时不等 timeout,用 BEGIN IMMEDIATE。

忘了 commit

实测 · 本机执行输出
  A 默认事务模式 INSERT 后没有 commit:A.in_transaction = True
  B(timeout=1)INSERT -> OperationalError: database is locked,等了 1.06 s
  A.commit() 之后 B 再 INSERT -> ok
  autocommit=True:A INSERT 之后 B INSERT -> ok,用时 0.3 ms

Python 默认事务模式下 INSERT 不 commit 会一直握着写锁。

多线程读后写

实测 · 本机执行输出
  DELETE 模式,timeout=0,BEGIN(默认延迟):成功写入 185/1600 行,失败 1415 次 {'database is locked': 1415},总用时 0.15 s
  DELETE 模式,timeout=5(Python 默认),BEGIN:成功写入 1600/1600 行,失败 0 次,总用时 0.76 s
  DELETE 模式,timeout=30,BEGIN IMMEDIATE:成功写入 1600/1600 行,失败 0 次,总用时 0.64 s
  WAL 模式,timeout=30,BEGIN IMMEDIATE:成功写入 1600/1600 行,失败 0 次,总用时 0.21 s
  WAL 模式,timeout=5,BEGIN(只改日志模式):成功写入 1600/1600 行,失败 0 次,总用时 0.17 s
  上面的事务只有一条 INSERT。真实代码常常是“先读再写”(SELECT 之后再 INSERT/UPDATE),下面加上 SELECT max(id) 再试:
  读后写:DELETE 模式,timeout=30,BEGIN:成功写入 1436/1600 行,失败 164 次 {'database is locked': 164},总用时 0.56 s
  读后写:DELETE 模式,timeout=30,BEGIN IMMEDIATE:成功写入 1600/1600 行,失败 0 次,总用时 0.95 s
  读后写:WAL 模式,timeout=30,BEGIN:成功写入 228/1600 行,失败 1372 次 {'database is locked': 1372},总用时 0.11 s
  读后写:WAL 模式,timeout=30,BEGIN IMMEDIATE:成功写入 1600/1600 行,失败 0 次,总用时 0.14 s

WAL + 调大 timeout 不够,读后写必须 BEGIN IMMEDIATE。

分步演示

锁
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64;报错文本都是真实复现时原样捕获的(命令行工具统一用 LC_ALL=C 取英文原文,路径换成 );所有服务都是本机上自己起的、只监听 127.0.0.1,不访问外网;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 Python 3.15.0 自带的 sqlite3 模块(SQLite 3.54.1);数据库文件在本机 APFS 的临时目录,实验结束后删除;多线程实验是 8 个线程、每线程一个连接、每行一个事务;数字(失败次数、用时)每次运行会有波动,结论是数量级和方向
    • 没有验证:多进程(而不是多线程)下的表现;网络文件系统(NFS、SMB)上的 SQLite 锁(官方不推荐);PRAGMA busy_timeout 与 Python timeout 参数的关系(这里只用了 Python 的 timeout);WAL 文件增长与 checkpoint 饥饿;SQLITE_BUSY_SNAPSHOT 的单独错误码(Python 只显示 database is locked);ORM(SQLAlchemy、Django)里各自的默认事务行为;SQLite 其它版本
    京ICP备2026067502号-1京公网安备11010502063487号