transaction.atomic() 是唯一靠谱的选择,因其自动处理保存点嵌套、异常捕获与回滚,而手动 commit/rollback 或 raw SQL 会绕过 Django 事务栈导致原子性失效。

为什么 transaction.atomic() 是唯一靠谱的选择
手动调用 commit() 或 rollback() 在 Django 里几乎必然出错——ORM 的懒加载、查询缓存、中间件钩子都会让事务边界失控。Django 的事务必须靠 transaction.atomic() 上下文管理器或装饰器来划定,它会自动处理保存点嵌套、异常捕获和回滚触发,其他方式无法保证原子性。
常见错误现象:IntegrityError 发生后账户余额“一增一减只完成一半”,或者并发请求下出现 DatabaseError: savepoint N does not exist。
- 不要在视图函数里写
connection.cursor().execute("BEGIN")—— 这绕过 Django 事务栈,atomic()不再感知它 - 避免跨 HTTP 请求维持事务(比如分两步 API 提交),数据库连接可能已关闭或被复用
- 如果用了多数据库,必须显式指定
using="default",否则默认只作用于default数据库
转账逻辑必须封装在同一个 atomic() 块内
哪怕只是读取两个账户余额再更新,所有 ORM 操作(get()、save()、update())都得包在同一个 atomic() 里。Django 不会自动把分散的 save() 聚合成一个事务,每个独立的 save() 默认是自动提交的(除非外层有 atomic())。
典型场景:用户 A 向 B 转账 100 元,A 余额扣减、B 余额增加、生成一条 TransferRecord 记录——三者缺一不可。
立即学习“Python免费学习笔记(深入)”;
from django.db import transaction
<p>def transfer_money(from_account, to_account, amount):
with transaction.atomic():</p><h1>必须先 re-fetch,防止并发读到过期值</h1><pre class="brush:php;toolbar:false;"> from_acc = Account.objects.select_for_update().get(id=from_account.id)
to_acc = Account.objects.select_for_update().get(id=to_account.id)
if from_acc.balance < amount:
raise ValueError("Insufficient balance")
from_acc.balance -= amount
to_acc.balance += amount
from_acc.save()
to_acc.save()
TransferRecord.objects.create(
from_account=from_acc,
to_account=to_acc,
amount=amount
)
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
注意 select_for_update():不加它,在高并发下可能读到旧余额并导致超扣;加了它才能真正锁住行,但仅对 PostgreSQL/MySQL InnoDB 有效。
并发转账时容易忽略的锁粒度与死锁风险
如果 A 转给 B、同时 B 转给 A,两个事务按不同顺序获取行锁(比如一个先锁 A 再锁 B,另一个先锁 B 再锁 A),就会触发数据库死锁。PostgreSQL 会主动终止其中一个事务并抛出 DatabaseError: deadlock detected,但 Django 不会自动重试。
- 统一锁顺序:始终按
id升序锁定账户,例如Account.objects.select_for_update().filter(id__in=[a_id, b_id]).order_by("id") - 避免在
atomic()块里做耗时操作(如发邮件、调外部 API),锁持有时间越长,死锁概率越高 - 不要依赖
get_or_create()的原子性来规避并发问题——它底层仍是先查后插,非真正原子
测试事务回滚是否真正生效
光看代码里写了 atomic() 不代表事务起作用。最容易被忽略的是:单元测试默认使用 TestCase,它本身用事务包裹每个 test 方法,导致你手动写的 atomic() 嵌套在测试事务里,异常发生时实际回滚的是整个 test 方法级事务,掩盖了业务逻辑中事务失效的问题。
验证方法:
- 改用
TransactionTestCase(它用 flush 替代事务回滚,能暴露真实行为) - 在视图中故意抛出未捕获异常(如
raise Exception("test rollback")),然后查数据库确认余额没变 - 用
logging打印transaction.get_connection().get_autocommit()确认进入atomic()后为False
事务不是魔法,它只保证“全成功或全失败”,但不会帮你解决余额校验竞态、网络超时重试、幂等设计这些更深层的一致性问题。

















