Django如何处理并发API请求,避免转账场景下database is locked报错
问题根因分析
你遇到的django.db.utils.OperationalError: database is locked报错核心是默认使用了SQLite数据库,它仅支持库级锁,只要有一个写事务在执行,其他所有写操作都会被直接阻塞报错,本身不适用于有并发写需求的生产场景。另外你当前的实现逻辑也有可优化空间。
可行解决方案(按优先级排序)
1. 更换支持行级锁的生产级数据库
直接换成MySQL/PostgreSQL这类支持行级锁的关系型数据库,是从根本上解决问题的方案:
- 这类数据库的
select_for_update默认是行级锁,只会锁定你查询到的对应用户的记录,不会锁整个库,其他用户的转账操作完全不受影响 - 同一个用户的并发请求,第二个请求会自动等待第一个事务释放锁后再执行,不会抛出全库锁异常,余额计算也不会出错
2. 优化锁逻辑,无需while循环等待
如果你不想让请求等待锁,可以直接调整锁参数+异常捕获,从入口拦截并发请求:
- 给
select_for_update添加nowait=True参数,拿不到锁时会直接抛出异常,不需要你自己实现循环等待逻辑 - 捕获拿锁失败的异常,直接返回「操作过于频繁,请稍后再试」的提示即可
- 如果是多实例部署,可额外增加Redis分布式锁做第一层拦截,以用户ID为key设置1-2秒的短过期时间,拿不到锁直接返回,不需要请求到数据库层
3. 优化余额存储逻辑
你当前每次从转账流水表倒查最新余额的设计效率低,也容易出问题,建议拆分出独立的UserAccount表存储用户实时余额,转账时直接锁定对应用户的UserAccount行做扣减,流水表仅作为操作记录事后对账使用。
优化后的代码示例
import datetime from django.db import transaction, DatabaseError # 建议拆分后的UserAccount模型方法 @transaction.atomic def withdraw(self, amount, phone, username): try: # 锁定当前用户的账户行,拿不到锁直接抛异常 user_account = UserAccount.objects.select_for_update(nowait=True).get(username=username) except DatabaseError: # 捕获拿锁失败的异常,直接返回提示 return {'result': 'error', 'message': '操作过于频繁,请稍后再试'} amount = int(amount) if user_account.balance >= amount: # 扣减余额 user_account.balance -= amount user_account.save() # 记录转账流水 transferLog.objects.create( username=username, _from=username, _to=phone, amount=amount, time=datetime.datetime.now() ) return {'result': 'success', 'message': 'transfer complete', 'remaining': user_account.balance} else: return {'result': 'error', 'message': 'not enough money'}
若必须保留SQLite的临时方案
如果暂时无法更换数据库,可在Django的settings.py中调整SQLite的锁等待超时时间,缓解报错问题:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'timeout': 20, # 单位为秒,调整锁等待最长时间 } } }
该方案仅为临时兼容,生产环境强烈建议更换为支持行级锁的数据库。
内容的提问来源于stack exchange,提问作者Foad
相关产品推荐
相关产品推荐

