Django 数据库事务:atomic、回滚边界与 on_commit
Django 数据库事务:atomic、回滚边界与 on_commit
类型:Django 官方文档 Managing database transactions 中文解读。本文保留官方对 autocommit、
atomic()、savepoint、on_commit()和异常处理的原意;示例为独立整理,不代表本博客应用已运行这些代码。[4]
Django 默认使用 autocommit
Django 默认运行在自动提交模式。没有活动事务时,每条查询通常会立即提交;当代码进入事务块后,提交边界由事务管理决定。Django 还会为某些需要多条查询保持完整性的 ORM 操作自动使用事务或 savepoint。[4]
自动提交适合简单的独立写操作,但不能替代业务事务。例如“创建订单、扣库存、记录支付状态”如果必须全部成功,就应该明确划定事务边界。
atomic() 保证一组操作整体提交或回滚
atomic() 既可以作为装饰器,也可以作为上下文管理器。正常退出时提交;异常离开时回滚。嵌套的 atomic() 通常使用 savepoint,内层成功不代表外层已经最终提交。[4]
from django.db import transaction
def create_order(user, product):
with transaction.atomic():
order = Order.objects.create(user=user, product=product)
product.stock -= 1
product.save(update_fields=["stock"])
return order
如果库存更新抛出异常,订单创建也会随外层事务回滚。这个结论要求数据库表和后端真正支持事务;数据库配置不能被忽略。
装饰器适合整个函数就是一个事务的场景:
from django.db import transaction
@transaction.atomic
def confirm_payment(order_id):
order = Order.objects.select_for_update().get(pk=order_id)
if order.status != Order.Status.PENDING:
return order
order.status = Order.Status.PAID
order.save(update_fields=["status"])
return order
select_for_update() 需要在事务中使用,并且具体锁行为依赖数据库后端。这里的代码表达的是常见订单幂等模式,不是对所有数据库的锁语义承诺。
不要在 atomic 内部吞掉数据库异常
Django 官方特别警告:不要在 atomic() 内部捕获并处理 DatabaseError,然后继续在同一个损坏的事务中执行查询。异常被隐藏后,离开事务时 Django 可能无法按代码作者预期判断回滚边界;数据库错误后继续查询还可能得到 TransactionManagementError。[4]
不推荐这样写:
from django.db import IntegrityError, transaction
with transaction.atomic():
try:
create_unique_record()
except IntegrityError:
# 事务可能已经处于需要回滚的状态
pass
# 这里继续查询或写入可能失败
do_more_work()
更清晰的做法是在外层捕获,或者增加一个内部事务块,明确哪些操作需要回滚:
from django.db import IntegrityError, transaction
try:
with transaction.atomic():
create_unique_record()
except IntegrityError:
handle_duplicate()
continue_with_normal_work()
若希望保留外层已完成的操作、只回滚可能失败的局部,可以使用嵌套 atomic() 形成 savepoint:
from django.db import IntegrityError, transaction
with transaction.atomic():
first.save()
try:
with transaction.atomic():
second.save() # 可能违反唯一约束
except IntegrityError:
record_duplicate()
third.save()
这里的 savepoint 让内部失败可以回到嵌套块进入前的状态,但外层事务仍然存在。异常边界要和业务意图保持一致,不能为了“让程序继续”而无条件吞掉错误。[4]
回滚不会自动恢复 Python 对象状态
数据库回滚只影响数据库状态,不会自动把内存里的模型字段恢复成原值。若代码在事务前后还修改了缓存、全局变量或其他外部状态,需要自行避免不一致,或者把外部副作用延迟到提交成功之后。[4]
from django.db import DatabaseError, transaction
obj = MyModel(active=False)
obj.active = True
try:
with transaction.atomic():
obj.save()
except DatabaseError:
# 数据库回滚不会自动改回 Python 对象属性
obj.active = False
这段代码只演示状态恢复。真正的缓存更新更适合放入 transaction.on_commit(),不要在事务尚未确认提交时提前把缓存改成“成功”状态。
on_commit() 处理提交后的副作用
邮件、异步任务和缓存失效等动作往往应该在数据库成功提交后才执行。on_commit() 注册的回调只有在外层事务成功提交后才执行;如果事务回滚,回调会被丢弃。没有活动事务时,回调会立即执行。[4]
from functools import partial
from django.db import transaction
def send_welcome_email(user_id):
print("send email for", user_id)
def create_user_and_notify(email):
with transaction.atomic():
user = User.objects.create(email=email)
transaction.on_commit(
partial(send_welcome_email, user_id=user.id)
)
return user
注意:回调发生在提交以后,不属于原事务。回调失败不会让已经提交的数据库事务重新回滚。对邮件这类可重试动作通常合理;对必须与数据库原子一致的动作,单靠 on_commit() 不足,需要重新设计可靠消息或两阶段提交等方案。[4]
回调按注册顺序执行。robust=True 可以让后续回调继续执行,但也意味着调用方要接受当前回调异常被记录而不是阻断后续回调的语义。[4]
按请求自动包事务不是免费功能
ATOMIC_REQUESTS=True 会在调用 view 前开启事务,view 正常返回则提交,抛异常则回滚。但 middleware 和模板渲染不在这个事务里;流式响应开始生成内容时,view 已经返回,后续生成代码可能运行在事务外。[4]
高流量应用给每个请求都开事务会增加开销,锁的影响取决于查询模式和数据库后端。不要因为配置简单就默认所有请求都适合 ATOMIC_REQUESTS。
# settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "app",
"ATOMIC_REQUESTS": True,
}
}
实际启用前,应先测量慢查询、锁等待、请求时长和连接池行为。事务应该尽量短,尤其是长时间运行的进程中。[4]
一个订单写入模式
下面把状态更新、库存扣减和提交后任务放在一起,展示边界。它仍然需要模型约束、数据库索引和任务幂等配合。
from django.db import transaction
def pay_order(order_id, payment_id):
with transaction.atomic():
order = (
Order.objects
.select_for_update()
.select_related("product")
.get(pk=order_id)
)
if order.status != Order.Status.PENDING:
return order
if Payment.objects.filter(provider_id=payment_id).exists():
raise ValueError("payment already used")
payment = Payment.objects.create(
order=order,
provider_id=payment_id,
)
order.status = Order.Status.PAID
order.save(update_fields=["status"])
transaction.on_commit(
lambda: enqueue_fulfillment(order_id=order.id)
)
return order
这个例子展示了几个原则:锁定需要串行判断的订单、让重复调用返回稳定结果、把外部任务放到提交之后。它没有替你解决支付回调签名验证、重复支付记录的数据库唯一约束、任务重试和对账;这些必须单独实现。
测试事务代码
Django TestCase 会用事务提供隔离,但测试中事务不一定真正提交,因此不能仅凭普通 TestCase 断言 on_commit() 已按生产时机运行。官方文档提供 captureOnCommitCallbacks(),也可以在需要真实提交语义时使用 TransactionTestCase,代价是测试更慢。[4]
from django.test import TestCase
from django.db import transaction
class OrderTests(TestCase):
def test_commit_callback(self):
with self.captureOnCommitCallbacks(execute=True) as callbacks:
with transaction.atomic():
transaction.on_commit(lambda: callbacks.append("done"))
self.assertEqual(callbacks, ["done"])
示例中的回调写法仅用于演示测试捕获机制;实际代码应避免把同一个列表同时当作 Django 捕获列表和业务结果容器。
结论
事务的难点不是记住 @transaction.atomic,而是确定:哪些数据库操作必须一起成功、异常在哪里捕获、外部副作用何时发生、数据库是否支持需要的锁和事务、测试是否真的覆盖提交路径。
本文没有在博客服务器部署 Django,也没有连接任何数据库执行示例。
来源
本文为中文技术解读,示例需要结合实际数据库后端验证。