解决Django中SQLite同时运行管理命令与/admin的数据库锁问题
Django + SQLite 数据库锁定问题解决方案
问题概述
- 运行长时间管理命令
./manage.sh download_company_data更新数据库时,Admin界面创建用户立即触发「数据库已锁定」错误 - 已设置数据库
timeout为20000,但未生效 - 管理命令中单个
update_or_create操作快速且不涉及User表,已添加重试逻辑、调用connections.close_all()并休眠1秒
解决办法
1. 修正SQLite超时配置的单位错误
SQLite的timeout参数单位是秒,而非毫秒。你当前设置的20000相当于5个多小时,可能导致配置解析异常无法生效。将超时调整为合理值(如20秒):
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'timeout': 20, # 等待20秒再抛出锁定错误 }, } }
2. 开启SQLite WAL模式提升并发能力
SQLite默认的DELETE日志模式下,写操作会独占整个数据库锁。开启Write-Ahead Logging(WAL)模式后,可支持多读单写的并发操作,大幅减少锁冲突概率:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'timeout': 20, 'init_command': 'PRAGMA journal_mode=WAL;' # 启用WAL模式 }, } }
注意:开启WAL后,需重启runserver和管理命令进程确保所有连接生效。
3. 确保管理命令的事务及时提交
Django默认会为ORM操作开启事务,若事务未及时提交,会持续持有数据库锁。用transaction.atomic()包裹单个更新操作,确保事务完成后立即释放锁:
from django.db import transaction for line in c: line = preprocess(line, prefix) search = { unique: line[unique] } for i in range(10): try: with transaction.atomic(): # 原子操作确保事务及时提交 Model.objects.update_or_create(**search, defaults=line) break except OperationalError as e: info('Operational error. Delayed retry...') sleep(0.02) connections.close_all() sleep(1)
4. 给Admin用户添加操作重试逻辑
即使优化了后端,偶尔的锁冲突仍可能发生。自定义Admin类,为用户创建操作添加重试机制:
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from django.db import OperationalError import time class RetryEnabledUserAdmin(UserAdmin): def save_model(self, request, obj, form, change): max_retries = 3 retry_delay = 0.5 # 每次重试间隔0.5秒 for attempt in range(max_retries): try: super().save_model(request, obj, form, change) return except OperationalError as e: if "database is locked" in str(e).lower(): if attempt < max_retries - 1: time.sleep(retry_delay) continue # 非锁定错误或重试耗尽,抛出原异常 raise # 替换默认的User Admin admin.site.unregister(User) admin.site.register(User, RetryEnabledUserAdmin)
5. 优化管理命令的连接使用
无需每次循环都调用connections.close_all(),改为仅关闭当前默认连接,避免不必要的连接重建开销:
# 替换 connections.close_all() 为 connections['default'].close()
内容的提问来源于stack exchange,提问作者hdante
相关产品推荐
相关产品推荐

