W weiserv
← 返回博客

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