春江暮客

春江暮客的个人学习分享网站

Python SQLite 报 database is locked:有限等待、短事务与故障测试

2026-10-16 技术
Python SQLite 报 database is locked:有限等待、短事务与故障测试

定时任务把结果写进 SQLite,偶尔却报 sqlite3.OperationalError: database is locked。把等待时间调大以后,错误可能减少,但任务也可能只是等得更久。

本文面向初学者到中级读者,提供一个短事务写入工具,并用两个独立进程复现锁冲突、验证失败退出码和解锁后的恢复。只操作新建的演示库,不需要安装依赖。

1. 方法:先缩短事务,再设置有限等待

SQLite 同时只允许一个写事务;第二个写入者必须等前一个提交或回滚。BEGIN IMMEDIATE 在事务开始时申请写入资格,因此能把竞争暴露在业务写入之前,见 SQLite 事务说明。它不会增加并行写入能力。

排查时先问三个问题:

  1. 是否有第二轮定时任务、交互终端或数据库工具还在写同一个文件?
  2. 是否把 HTTP 请求、模型推理或文件转换放在写事务里面?
  3. 失败后是否仍保留未结束的事务或长时间打开的查询?

对于可恢复的 SQLite 批处理,先在事务外准备结果,再用短事务提交一条或一小批记录。下面按 job 键更新结果;它只保证数据库中的这次更新,不保证外部工作只执行一次。

2. 准备:创建独立演示库

需要 Python 3.11 或更新版本和本地可写目录。以下示例已在 Linux、Python 3.11.4、SQLite 3.41.2 下测试。重复实验时使用新的目录名,后续命令在同一个目录执行:

python3 --version
python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'
mkdir sqlite-lock-demo
cd sqlite-lock-demo
python3 - <<'PY_SETUP'
import sqlite3
from contextlib import closing
from pathlib import Path

db = Path("demo.sqlite")
if db.exists():
    raise SystemExit("Use a fresh demo directory")
with closing(sqlite3.connect(db, isolation_level=None)) as con:
    con.execute("CREATE TABLE results (job TEXT PRIMARY KEY, value TEXT NOT NULL)")
print(db.resolve())
PY_SETUP

第一条版本检查确认 Python 版本,第二条显示它实际链接的 SQLite 版本。演示默认使用新数据库的回滚日志模式,不启用 WAL,也不调整服务器配置。

3. 实现:准备好结果后再进入事务

把下面代码保存为当前目录的 save_result.py:

"""Save one prepared result in an existing SQLite database. Python 3.11+."""
import argparse
from contextlib import closing
import math
from pathlib import Path
import sqlite3
import sys


def wait_seconds(text):
    value = float(text)
    if not math.isfinite(value) or not 0 <= value <= 30:
        raise argparse.ArgumentTypeError("timeout must be finite and in 0..30 seconds")
    return value


def save_result(database, job, value, *, timeout=1.0):
    path = Path(database).resolve(strict=True)
    # mode=rw prevents accidental creation if the path disappears before opening.
    with closing(sqlite3.connect(
        path.as_uri() + "?mode=rw", uri=True,
        timeout=timeout, isolation_level=None,
    )) as con:
        try:
            con.execute("BEGIN IMMEDIATE")
            con.execute(
                "INSERT INTO results(job, value) VALUES (?, ?) "
                "ON CONFLICT(job) DO UPDATE SET value=excluded.value",
                (job, value),
            )
            con.execute("COMMIT")
        except BaseException:
            if con.in_transaction:
                con.execute("ROLLBACK")
            raise


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("database", type=Path)
    parser.add_argument("job")
    parser.add_argument("value")
    parser.add_argument("--timeout", type=wait_seconds, default=1.0)
    args = parser.parse_args()
    try:
        save_result(args.database, args.job, args.value, timeout=args.timeout)
    except (OSError, sqlite3.Error) as exc:
        name = getattr(exc, "sqlite_errorname", type(exc).__name__)
        print(f"ERROR: {name}: {exc}; db={args.database.resolve()}", file=sys.stderr)
        return 1
    print(f"OK: job={args.job}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

isolation_level=None 让这里由显式 SQL 控制事务;不要把这段代码混入另一个已经开始的事务。closing() 确保连接关闭。Python 事务控制文档说明了这种用法。

timeout 单位是秒,控制遇到锁竞争时的等待;它不是整个脚本的截止时间,也不是 SQL 执行时长限制。SQLite 的 busy timeout 说明解释了等待重试的行为。这里不额外叠加无限重试,锁一直存在就返回失败。

调用时先完成耗时计算,再把准备好的结果传给 save_result()。写入成功返回 0,数据库或文件错误返回 1,命令行参数错误返回 2;--timeout 只接受 0 到 30 秒内的有限值。mode=rw 和严格路径检查避免将拼错的路径静默创建成空库。

4. 验证正常写入与重复更新

python3 save_result.py --help
python3 save_result.py demo.sqlite daily ready --timeout 1
python3 save_result.py demo.sqlite daily updated --timeout 1
python3 - <<'PY_CHECK'
import sqlite3
from contextlib import closing

with closing(sqlite3.connect("file:demo.sqlite?mode=ro", uri=True)) as con:
    print(con.execute("SELECT job, value FROM results ORDER BY job").fetchall())
PY_CHECK

末尾应看到:

OK: job=daily
OK: job=daily
[('daily', 'updated')]

两次使用相同 job,只保留一条记录。SQL 参数用 ? 绑定,值里的引号或中文不需要手工拼进 SQL。

5. 复现锁冲突,验证释放与回滚

把下面代码保存为 test_lock.py。它会修改演示库,将最终的 daily 值改为 recovered;只在前面创建的演示目录运行:

"""Hold a real write lock while a separate process attempts to write."""
from contextlib import closing
from pathlib import Path
import sqlite3
import subprocess
import sys

from save_result import save_result

db = Path("demo.sqlite").resolve(strict=True)
command = [sys.executable, "save_result.py", str(db), "daily", "after-lock",
           "--timeout", "0.2"]
with closing(sqlite3.connect(db, isolation_level=None)) as holder:
    holder.execute("BEGIN IMMEDIATE")
    try:
        holder.execute("INSERT INTO results VALUES ('uncommitted', 'discard-me')")
        blocked = subprocess.run(command, capture_output=True, text=True, timeout=10)
        assert blocked.returncode == 1, blocked
        assert "SQLITE_BUSY" in blocked.stderr, blocked.stderr
        assert blocked.stdout == "", blocked.stdout
        print("PASS: competing writer returned 1 with SQLITE_BUSY")
    finally:
        holder.execute("ROLLBACK")

released = subprocess.run(command, capture_output=True, text=True, timeout=10)
assert released.returncode == 0, released.stderr
assert released.stdout.strip() == "OK: job=daily", released.stdout

with closing(sqlite3.connect(db, isolation_level=None)) as con:
    assert con.execute("SELECT value FROM results WHERE job='daily'").fetchone() == (
        "after-lock",
    )
    assert con.execute("SELECT count(*) FROM results WHERE job='uncommitted'").fetchone() == (0,)

try:
    save_result(db, "daily", None)
except sqlite3.IntegrityError:
    pass
else:
    raise AssertionError("NOT NULL constraint should fail")

save_result(db, "daily", "recovered")
with closing(sqlite3.connect(db)) as con:
    assert con.execute("SELECT job, value FROM results").fetchall() == [("daily", "recovered")]
print("PASS: release, rollback, repeat update, and final rows verified")

运行:

python3 test_lock.py

预期输出:

PASS: competing writer returned 1 with SQLITE_BUSY
PASS: release, rollback, repeat update, and final rows verified

父进程一直持锁,直到子进程的写入失败并返回;这样不依赖两个终端碰巧同时运行。子进程应在约 0.2 秒的锁等待后失败,调度开销会影响实际耗时,不应断言精确毫秒数。实验还检查未提交记录已回滚、约束错误后的连接已释放,以及最终行值正确。

测试脚本返回 0 表示这些断言通过;其中一次被测写入故意返回 1。正式任务遇到这个失败时应保留非零退出码,不能打印成功后继续。

6. 常见问题与修复

现象 具体操作
总是 SQLITE_BUSY 检查重叠任务和未提交事务;把网络请求、sleep 和耗时计算移到 BEGIN 之前。
增大 timeout 仍失败 找出长期占锁的工作;先缩短事务或将写入排队给单个写入者,再按任务时限选择等待值。
cannot start a transaction within a transaction 检查外层是否已经开启事务;本文函数自行创建连接,不要直接复制其 BEGIN 到已在事务中的连接。
no such table: results 确认使用第 2 节初始化的演示库;正式工具应使用自己的表结构和迁移流程。
只读或目录权限错误 核对报错中的绝对路径及运行账号,确保数据库和需要写入日志的父目录可写;不要直接全目录 chmod 777。

WAL 能改善读写并发,但仍然只有一个写入者,见 SQLite WAL 并发说明。因此本文不把切换 WAL 当作锁冲突的通用修复,也不使用 check_same_thread=False 解决跨进程竞争。

不要删除 -journal、-wal 或 -shm 文件来“解锁”。需要保存现场时,参考SQLite 备份与恢复验证,避免只复制一个正在使用的主文件。整个外部命令还需要独立运行时限时,可参考子进程超时与日志。

总结

先把耗时准备工作移出写事务,再设置有限的锁等待,并在异常时回滚、关闭连接。用真实持锁进程检查失败退出码,再验证锁释放后能写入、最终行值正确。若写入持续拥挤,就需要单写入者队列或重新评估存储方案,单纯延长 timeout 不能增加 SQLite 的写入并发能力。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它