Django 1.11+PostgreSQL事务内是否存在竞态条件?
竞态条件分析与解决方案
你担心的点确实是对的——这段代码存在竞态条件,而且当前的transaction.atomic()配置没法完全解决这个问题,我来拆解一下:
为什么会有竞态条件?
看你的代码逻辑:
- 先从数据库查询出一批
Account对象(accounts = Account.objects.filter(id__in=account_ids)) - 循环每个account时,先读取了
account.hours的值,再进入原子事务 - 事务内用这个预先读取的
account.hours创建日志,然后把该账户的hours更新为0
假设有两个请求同时处理同一个账户ID:
- 请求A先查询到账户的hours是10,进入事务但还没执行更新
- 请求B在请求A更新前,也查询到同一个账户的hours是10,进入事务
- 最后两个请求都创建了
diff=-10的日志,并且都把账户hours设为0
结果就是:账户实际只应该被扣10小时,但日志里记录了两次扣10,数据一致性被破坏了。问题出在account.hours是在事务外读取的,这个值不是事务内的最新状态。
当前的transaction.atomic()能解决什么?
它能保证事务内的两个操作(创建MyLog和更新Account)是原子的——要么两个都成功,要么都失败,不会出现“日志创建了但账户没更新”或者反过来的情况。但它没法解决事务开始前读取数据带来的竞态,因为读取操作不在事务的原子范围内。
怎么修复这个问题?
有两种常用的可靠方案:
方案1:在事务内重新获取账户数据并加锁
把读取账户数据的操作放到事务内,同时用select_for_update()给行加锁,防止其他事务修改:
from django.db import transaction def write_off(account_ids): for account_id in account_ids: with transaction.atomic(): # 在事务内加锁查询最新的账户数据 account = Account.objects.select_for_update().get(pk=account_id) MyLog.objects.create( hours=0, operation_type=LOG_OPERATION_WRITE_OFF, diff=-account.hours, ) Account.objects.filter(pk=account_id).update(hours=0)
select_for_update()会锁定查询到的行,直到当前事务结束,其他事务想要修改或者加锁同一行时会被阻塞,这样就能保证读取的account.hours是最新的,而且不会有其他事务在中间修改它。
方案2:用F表达式优化先读后写逻辑
如果业务允许,也可以用Django的F()表达式直接更新,避免先读取数据再更新的竞态(不过如果需要记录具体扣除数值,方案1更稳妥):
from django.db import transaction, F def write_off(account_ids): for account_id in account_ids: with transaction.atomic(): # 先加锁获取旧值,确保日志记录准确 account = Account.objects.select_for_update().get(pk=account_id) # 用F表达式更新(这里其实直接设为0也可以,F表达式适合增减场景) Account.objects.filter(pk=account_id).update(hours=0) MyLog.objects.create( hours=0, operation_type=LOG_OPERATION_WRITE_OFF, diff=-account.hours, )
总结一下:你担心的竞态条件确实存在,当前的transaction.atomic()没覆盖到事务外的读取操作,需要把读取操作放到事务内并加锁,才能彻底避免数据不一致的问题。
内容的提问来源于stack exchange,提问作者v18o
相关产品推荐
相关产品推荐

