sqlite3.OperationalError: database is locked 并发写锁冲突的排查与修复
报错与症状
在本地脚本或小型 Web 服务里用 sqlite3 写入时,偶尔会遇到:
sqlite3.OperationalError: database is locked
这个报错不挑 Python 版本,2.x / 3.x 都会出现,常见于「多线程同时写」或「一个写事务长时间不提交、另一个连接又来写」的场景。
触发环境
- Python 3.13.12(本机实测,Windows 11;Linux / macOS 行为一致)
- sqlite3 标准库(无需任何第三方依赖)
- 多连接 / 多线程访问同一个
.db文件
根因分析
SQLite 的写操作是数据库级排他锁:同一时刻只允许一个写者。当连接 A 通过 BEGIN IMMEDIATE(或自动事务)拿到写锁后,在它提交或回滚之前,其它连接尝试写会进入等待;等待超过 timeout(默认 5 秒)仍未拿到锁,就抛出 database is locked。
关键点:这不是「数据坏了」,而是「锁没腾出来」。本质原因是写事务持有时间过长,或者并发写入模型设计不当。
最小复现(可运行)
下面这段代码让连接 A 持有写锁、未提交,同时让连接 B 以 timeout=0(不等待)立即写入,必然复现:
# 运行环境:Python 3.13.12 / Windows 11 / sqlite3 标准库
import sqlite3, tempfile, os
path = tempfile.mktemp(suffix=".db")
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE IF NOT EXISTS t(id INTEGER)")
conn.execute("BEGIN IMMEDIATE") # A 拿到写锁
conn.execute("INSERT INTO t VALUES (1)") # 写入但不提交
conn2 = sqlite3.connect(path, timeout=0) # B 不等待
try:
conn2.execute("INSERT INTO t VALUES (2)")
print("未报错(意外)")
except sqlite3.OperationalError as e:
print("捕获到 sqlite3.OperationalError:", repr(e))
conn.rollback(); conn.close(); conn2.close()
os.remove(path)
终端输出(实测)
捕获到 sqlite3.OperationalError: OperationalError('database is locked')
解决方法(按推荐优先级)
1. 提高 busy_timeout(最省事,推荐首选)
让写入方在拿不到锁时自动等待一段时间,而不是立刻报错:
conn = sqlite3.connect(path)
conn.execute("PRAGMA busy_timeout = 5000") # 最多等待 5 秒
对绝大多数「偶发冲突」场景,这一步就能消除报错。
2. 串行化写操作
用单连接,或连接池 + 写锁(如 threading.Lock)保证同一时刻只有一个写者。Web 场景可借助同步原语让写请求排队,避免多个线程争抢同一个文件锁。
3. 启用 WAL 模式(高并发读写分离)
conn.execute("PRAGMA journal_mode = WAL")
WAL 允许「一个写者 + 多个读者」并发,读不阻塞写、写不阻塞读,显著降低锁冲突概率。注意 WAL 需要本地文件系统支持,部分网络盘或只读容器层不适用。
预防措施
- 写事务越短越好:把
INSERT / UPDATE集中执行并尽快commit(),不要在事务里做网络请求或人工等待。 - 默认
timeout=5.0已能缓解大部分偶发冲突,但频繁报错说明并发模型需要重构,别只靠调大 timeout 掩盖问题。 - 生产环境若高并发写入,认真考虑换 PostgreSQL / MySQL 这类支持行级锁的数据库。
本文环境:Django 4.2 / Python 3.11 / Ubuntu 22.04
广告位占位 · post-inline