DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

SQLite WAL 报 SQLITE_BUSY_SNAPSHOT,增加 busy_timeout 为什么没用

核对517过期快照、完整事务重启与重新计算,评估读取前IMMEDIATE的占锁代价,保留游标边界。

同一个 SQLite 连接先 BEGIN、SELECT,另一个连接写入并提交,原连接再 UPDATE 时立即报 database is locked。即使结果已经读完、另一个写入者也已结束,扩大 busy_timeout 仍不能让这次写入成功。先取得扩展错误码,确认是否为 SQLITE_BUSY_SNAPSHOT,也就是 517。

这条路径针对 WAL 模式下仍打开的读事务升级为写事务。WAL 从 SQLite 3.7.0 起提供,但具体驱动、错误码显示和事务管理仍须按实际版本核对。database is locked 这句文字不能独自区分普通忙等待、过期快照和其他锁问题。

另一个写入已经结束,旧快照仍然过期

普通 BEGIN 默认采用 DEFERRED 行为,真正的读取事务在首次访问数据库时建立。连接 A 开始读事务并取得快照,连接 B 随后提交修改;A 继续读取时,仍看到自己事务中的旧快照。

如果 A 现在尝试写入,SQLite 不能让它从这个旧快照分出另一段数据库历史,因此拒绝升级。等待 B 释放写锁不能更新 A 已经持有的快照,这与 B 仍占着写锁、稍后可以取得锁的情况不同。

记录 A 的事务开始、首次读取,B 的写入提交以及 A 的失败位置,同时保存主错误码和扩展错误码。517 属于主错误 SQLITE_BUSY 的扩展码,不能把界面中的“locked”直接认定为主错误 SQLITE_LOCKED。

以下可以在已经获准的目标连接中只读核对 SQLite 版本与日志模式;它不会切换日志模式:

SELECT sqlite_version();
PRAGMA journal_mode;

查询版本应来自应用实际加载的 SQLite,而不只是另一台机器的 CLI。较新的 CLI 改善了错误码显示,不代表升级显示工具就改变了快照隔离规则。

结束旧事务,重新读取后再决定写什么

已确认是过期快照时,由拥有事务的业务代码结束这个旧事务,再开启新的事务、重新读取数据并重新计算写入。不能只在同一个旧事务里重发最后一条 UPDATE。

回滚会丢弃这笔事务中尚未提交的修改,所以应先核对事务范围。新读取可能得到不同的数据,业务条件、余额或版本检查都要重新判断;直接保留基于旧快照计算出的结果,换个事务写回,也可能覆盖其他连接已经提交的变化。

重试应有次数和时间限制,并覆盖正确的业务单元。数据库回滚不撤销已发送的消息、外部付款或其他副作用,不能把这些操作连同 SQL 一起无条件重做。由驱动或 ORM 管理事务时,沿用它的结束和开启接口,不在包装层背后随意发送 BEGIN。

读完或关闭游标,也不一定结束显式事务。关于游标尚未释放的另一条锁路径,可对照SQLite DataReader 与事务生命周期;如果显式事务还在,单独关闭 reader 不会刷新它的快照。

已知会写入,可以评估在读取前使用 IMMEDIATE

业务明确需要先读后写时,可以在事务开始、首次读取之前评估 BEGIN IMMEDIATE。它提前取得写事务,避免先建立一个稍后可能无法升级的读快照。

下面只展示事务开始语句,应合入已有事务流程,不能在另一个已打开的事务里直接执行:

BEGIN IMMEDIATE;

它本身可能因为其他写事务返回 SQLITE_BUSY,并会阻止其他连接成为写入者。事务应保持短,不跨网络调用、用户等待或长时间计算。不要为这一种故障把所有只读查询都改成 IMMEDIATE,也不能省略业务完成后的提交或失败后的回滚。

busy_timeout 可以用于允许等待的锁竞争,但不能让旧快照变新。它还会替换连接上已有的 busy handler,影响等待和上层超时;有些可能形成死锁的竞争不会调用 handler,不能承诺设置了超时就一定等待指定时长。

修改前保存事务管理代码,在隔离数据中覆盖普通写锁竞争和 A/B 快照升级两种顺序。验收要确认失败事务已经结束、新读取取得当前数据、重新计算后的写入符合业务,并且没有重复副作用。不要删除 -wal、-shm 来解锁;它们可能包含数据库恢复或尚未写回的数据。