跳到主内容
快讯直播
AI智模界
教程

SQLite database is locked 报错的原因与解决

报错现象

不同语言和工具里,这句话长得略有差别,但关键字都是 database is locked

```text

Python

sqlite3.OperationalError: database is locked

命令行

Error: database is locked

Go(mattn/go-sqlite3 等驱动)

database is locked (5) (SQLITE_BUSY)

Node.js(better-sqlite3)

SqliteError: database is locked

```

它通常在下面这些操作中出现:

  • 两个进程或两个线程同时对同一个 .db 文件执行 INSERT/UPDATE/DELETE,其中一个抛错;
  • Web 服务里某个请求在事务中做了耗时的事(调外部接口、sleep、逐条处理大批数据),同一时间别的请求写库失败;
  • sqlite3 命令行或某个数据库图形工具打开了库、开着事务没退出,业务进程写不进去;
  • 写一个批量导入脚本,同时后台定时任务也在写同一张表;
  • 特征往往是「间歇性」:同一条语句重试一次就过了,过一会儿又报。

影响范围要分两种情况看:

1. 默认的 rollback journal(回滚日志)模式下,写事务持锁期间,读操作也可能失败,报同样的错;

2. 开启 WAL 模式后,读写可以并发,读不再被写阻塞,但同一时刻仍然只允许一个写事务,两个写操作照样会撞。

另外,报错的那条语句失败不代表事务一定干净回滚了,事务中断后要确认数据状态,别默认「没写进去」。

可能原因

按实际遇到的比例从高到低排:

1. 并发写:多个连接同时想发起写事务。SQLite 的写锁是数据库文件级别的,一次只能一个。

2. 事务开了没关:代码里 BEGIN 之后没有 COMMITROLLBACK,异常路径漏了,连接一直挂着写锁。

3. busy_timeout 太小或为 0:默认情况下,遇到锁是直接报错而不是等待。很多语言绑定的默认等待时间很短甚至没有。

4. 还在用回滚日志模式:没开 WAL,读写互相阻塞,稍微有点并发就撞。

5. 长事务、大批量写:一个事务里塞了几万条 INSERT,或者事务里夹着网络请求。

6. 锁升级死锁:事务先读后写,读锁升级写锁时发现别人已经拿了写锁,双方互等。

7. 数据库放在网络盘上:NFS、SMB 之类的共享目录,SQLite 的锁机制不可靠。

8. 连接跨线程复用:一个连接被多个线程共用,或者在连接池里被反复借还。

9. 目录权限问题:数据库文件可写,但所在目录不可写,创建不了 -journal-wal-shm 这些辅助文件。

10. 别的程序占着文件:图形化工具、备份脚本、同步网盘客户端、杀毒软件扫描。

逐条排查与解决

第 1 条:并发写

判断方法:先用命令行确认是不是「有人占着」。

```bash

Linux / macOS,看谁打开了这个文件

lsof app.db

或者

fuser app.db

```

Windows 可以用 Sysinternals 的 handle.exe,或者用资源监视器的「搜索句柄」功能,输入 .db 文件名。

如果 lsof 列出来一堆进程,那就是并发写的问题。解决思路只有一个:把写操作串行化。可以是一个专门的写线程,或者一个单写者队列,所有写入都排队执行。读操作在 WAL 模式下可以并发。

第 2 条:事务没提交

判断方法:把业务代码停掉,然后用命令行打开数据库试试写:

```bash

sqlite3 app.db "CREATE TABLE IF NOT EXISTS t(x); INSERT INTO t VALUES(1);"

```

如果业务停了命令行还报 locked,说明是别的进程占着;如果命令行能写,说明锁是业务进程自己持有的。

代码上重点检查:BEGINCOMMIT/ROLLBACK 是否成对;异常分支是不是漏了 rollback;有没有在事务里做耗时操作。用 try/finally 包住:

```python

conn.execute("BEGIN IMMEDIATE")

try:

conn.execute("INSERT INTO t VALUES (?)", (1,))

conn.execute("COMMIT")

except Exception:

conn.execute("ROLLBACK")

raise

```

第 3 条:busy_timeout

重要前提:busy_timeout每个连接的设置,不是数据库文件的全局设置,所以每建一个连接都要设。

命令行查看和设置:

```bash

sqlite3 app.db "PRAGMA busy_timeout;" # 查看当前值(毫秒)

sqlite3 app.db "PRAGMA busy_timeout=5000;" # 设置为 5 秒

```

命令行里还可以用点命令:

```bash

sqlite3 app.db ".timeout 5000"

```

代码里:

```python

import sqlite3

conn = sqlite3.connect("app.db", timeout=10) # 单位是秒

conn.execute("PRAGMA busy_timeout=10000") # 单位是毫秒

```

设了之后,遇到锁会先等待而不是立刻报错。设多大算合适?看业务能接受的最长阻塞时间,一般几秒量级。注意别设得过大,否则请求会整体被拖慢。

第 4 条:WAL 模式

journal_mode持久化在数据库文件里的,设置一次就行,新开的连接会沿用。但设置时需要没有其他连接在活动。

```bash

sqlite3 app.db "PRAGMA journal_mode=WAL;"

```

看返回值:返回 wal 表示成功,返回 delete 表示没设上(常见于有别的连接在用,或者文件系统不支持)。

```bash

sqlite3 app.db "PRAGMA journal_mode;" # 查看当前模式

```

设为 WAL 后,同目录会出现 app.db-walapp.db-shm 两个文件,这是正常的,不要手动删。目录必须可写,否则会报错。

第 5 条:长事务

判断方法:把写入改成每 500 或 1000 条提交一次,看报错是否消失。是的话就是事务太长。

分批提交:

```python

for i, row in enumerate(rows):

conn.execute("INSERT INTO t VALUES (?)", (row,))

if i % 1000 == 0:

conn.execute("COMMIT")

conn.execute("BEGIN IMMEDIATE")

conn.execute("COMMIT")

```

第 6 条:锁升级死锁

典型场景:事务先 SELECT(拿到读锁),后面再 UPDATE(想升级成写锁),而另一个连接已经拿了写锁在等读锁释放——双方互等。这种情况下 busy_timeout 帮不上忙,会立刻失败。

解决办法是在事务一开始就用 BEGIN IMMEDIATE 直接拿写锁:

```python

conn = sqlite3.connect("app.db", isolation_level=None, timeout=10)

conn.execute("BEGIN IMMEDIATE")

```

注意 isolation_level=None 是为了关掉 Python 驱动自己的事务管理,避免手动 BEGIN 时报 cannot start a transaction within a transaction

第 7 条:网络盘

判断方法:

```bash

df -T /path/to/app.db # Linux,看文件系统类型

mount | grep /path # macOS

```

如果挂在 NFS、SMB、CIFS 之类的共享存储上,建议把数据库挪到本地磁盘。SQLite 官方文档也说明它依赖文件锁,在网络文件系统上不可靠。这一点改配置解决不了,只能换存储位置。

第 8 条:连接跨线程

判断方法:看日志里报错的线程和建连接的线程是不是同一个。原则是一个线程一个连接,或者干脆所有数据库操作走同一个线程。Python 里 check_same_thread=False 能绕过检查,但不会让连接变安全。

第 9 条:目录权限

判断方法:

```bash

ls -ld $(dirname /path/to/app.db)

ls -l /path/to/app.db*

touch /path/to/test_write && rm /path/to/test_write

```

数据库文件本身可写不够,所在目录也必须可写,因为要创建日志文件。用 chmod/chown 修好目录权限。

第 10 条:别的程序占着

判断方法:lsofhandle.exe 再看一遍,重点排查图形化数据库工具、备份任务、网盘同步进程。把它们停掉或换到不影响业务的时段跑。

都不管用时的兜底方案

方案一:给写操作加重试

大多数 locked 是瞬时的,带退避的重试能消化掉很大一部分:

```python

import time, random, sqlite3

def retry(fn, attempts=5):

for i in range(attempts):

try:

return fn()

except sqlite3.OperationalError as e:

if "locked" not in str(e).lower() or i == attempts - 1:

raise

time.sleep(min(0.05 * (2 ** i), 1.0) + random.random() * 0.05)

```

配合 BEGIN IMMEDIATE 使用时,重试要重试整个事务,不能只重试中间那一条语句。

方案二:彻底单写者化

把所有写操作收敛到一个进程或一个线程里,其他部分通过队列把写请求投递过去。这是从根上解决问题,代价是要改架构。

方案三:先备份再重建

如果怀疑文件本身有问题(比如残留的 -wal 文件、磁盘写满留下的半截数据):

```bash

sqlite3 app.db "PRAGMA integrity_check;"

sqlite3 app.db "VACUUM INTO 'app_backup.db';"

```

VACUUM INTO 会生成一个干净的副本。确认无误后再替换原文件,替换前务必停掉所有连接。

方案四:换数据库

如果业务确实需要高并发写(比如多个服务实例同时写同一张表),SQLite 的设计目标就不是这个场景。这时候评估换 PostgreSQL 或 MySQL 更实际。SQLite 更适合读多写少、单机部署的场景。

如何预防再次发生

1. 统一连接初始化。封装一个 get_conn(),里面固定做这几件事:

```python

def get_conn(path="app.db"):

conn = sqlite3.connect(path, timeout=10, isolation_level=None)

conn.execute("PRAGMA journal_mode=WAL")

conn.execute("PRAGMA busy_timeout=10000")

conn.execute("PRAGMA synchronous=NORMAL")

conn.execute("PRAGMA foreign_keys=ON")

return conn

```

这样新加代码时不会漏设。

2. 事务要短。事务里不放网络请求、文件 IO、sleep。批量写就分批提交。

3. 先写后读,或者一开始就用 BEGIN IMMEDIATE。避免锁升级。

4. 写入口收敛。哪怕做不到完全单写者,至少让所有写入走同一个模块,方便加日志和重试。

5. 监控 WAL 文件大小app.db-wal 异常膨胀通常说明有长事务或 checkpoint 没跟上。

6. 压测时模拟并发。开发阶段写个脚本开多个连接同时写,观察是否报 locked,比上线后才发现要好。

7. 数据库文件放本地磁盘,别放共享目录。

8. 定期备份并验证VACUUM INTO 或者 .backup 都行,关键是定期做、并且验证备份文件能打开。具体参数以 SQLite 官方文档当前版本为准。

最后提醒:locked 这个错误本身信息量不大,真正有用的是「谁持有锁、持有多久」。排查时先定位持有者,再决定是改配置、改代码还是改架构。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。