DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

SQLite 查询后写入报 database is locked:检查未关闭的 DataReader

连接和 command 已关闭仍可能有读取路径未结束;检查 reader、显式事务及外部编辑,区分忙等待和 WAL 的能力边界。

SQLite 查询可以执行,随后的写入却报 database is locked,而代码看起来已经关闭了连接和命令。使用 System.Data.SQLite 时,还应检查读取数据的 SQLiteDataReader 是否真正结束;只处理写入函数,不一定能找到仍占着事务的读取路径。

这个场景尤其值得检查那些只读取一行、检查 HasRows 后提前返回,或把 reader 交给其他代码继续消费的函数。数据库锁还有其他原因,不能仅凭错误就认定存在 reader 泄漏。

看读操作是否在所有退出路径中释放

检查每处 ExecuteReader(),确认返回的 reader 有明确的生命周期。连接、command 和 reader 是不同对象;不能把“command 使用了 using”当成 reader 已被单独管理。

以下是 System.Data.SQLite 的读取结构示例,不包含实际业务查询。ConnectionString 和 sql 应由已有项目提供,涉及外部输入的 SQL 需要参数化:

using (var connection = new SQLiteConnection(ConnectionString))
{
    connection.Open();
    using (var command = new SQLiteCommand(sql, connection))
    using (var reader = command.ExecuteReader())
    {
        while (reader.Read())
        {
            // 在此读取需要的数据,不把 reader 返回给调用方。
        }
    }
}

如果只想判断是否有行,也应在 reader 的 using 块中读取 reader.HasRows,而不是创建一个新的 reader 后直接返回。异常、提前返回和取消任务都需要考虑;只在函数末尾写 Close(),可能有路径根本到不了那里。

对于自动开始的事务,最后一个活动语句结束后才会自动提交。未关闭的游标可能让语句继续保持活动,因此连接仍存在并不等于它必然阻塞写入,关键是它是否持有尚未结束的事务或语句。

显式事务不会因 reader 关闭自动结束

如果代码自己开始事务,关闭 reader 只是结束读取语句,仍需按业务要求提交或回滚事务。记录事务开始和结束位置,查看是否跨越了用户等待、网络请求或长时间计算。

SQLite 可以同时存在多个读事务,但只能有一个写事务。其他连接已有写入时,当前连接尝试升级读事务也可能失败。这个限制不是“SQLite 只能打开一条连接”,不要为排错把正常的多连接读操作全部删掉。

修改读取函数之前保留代码版本和业务行为,确认原先的流式读取是否真的需要跨函数存在。若要改成先读取到内存再释放 reader,应评估数据量;不能把大结果集不加限制地全部加载。

排除数据库浏览器和其他写入者

应用的 reader 已结束却仍锁住时,检查是否有数据库浏览器正在编辑同一个文件、另一个服务正在写入,或后台任务仍未结束事务。数据库浏览器中的未提交修改,应先核对内容,再选择保存或撤回;撤回会丢弃本次尚未保存的编辑,不能未经检查替它提交。

应用侧可以把需要保持同一业务原子性的写入组织成短事务,并协调多个写入任务。不要把一个必须全部成功或全部失败的事务改成逐条自动提交,只为了让锁错误暂时消失。

忙等待超时只能让连接等锁释放,并不会关闭长期占锁的 reader 或提交其他事务。设置等待时长还会影响请求延迟,应结合上层超时和重试逻辑;不是所有锁相关错误都能通过增加等待时间解决。

WAL 不能提供多个同时写入者

WAL 模式通常允许读和写同时进行,但仍然只有一个写入者。长读事务还可能拖住 checkpoint。因此,先修正语句与事务生命周期,再评估是否需要改变日志模式。

切换模式会影响数据库文件及辅助文件的管理,并有运行环境限制。不要随手删除 -wal、-shm 或 journal 文件来“解锁”;它们可能包含恢复所需的状态或尚未写回主文件的数据。

修复后,在原来的查询后写入场景中确认 reader 和事务都按预期结束,原写入不再因同一持锁路径失败。还要检查读到的数据、写入原子性和异常回滚结果;重启后暂时恢复不能证明泄漏已经消除。