基于 Django REST Framework 打造电商后端(下):事务控制、缓存设计与异步任务落地
摘要:
上篇主要介绍了这个 Django 电商后端项目的整体架构、模块划分和核心业务模型。这一篇继续往里拆,重点分析订单提交流程为什么要用事务、订单取消为什么要抽成服务层、Redis 缓存是如何做自动失效的,以及 Celery 是怎么实现订单通知和超时自动取消的。最后再结合测试用例,看看这个项目是怎么把关键链路兜住的。
关键词: Django REST Framework、事务、Redis、Celery、订单系统、缓存失效、自动化测试
一、下篇重点看什么
如果说上篇解决的是“项目是怎么搭起来的”,那下篇重点回答的就是“这个项目为什么这样写”。
电商后端最容易出问题的地方,通常不在商品列表,也不在登录注册,而是在订单链路。因为订单会同时影响多个对象:购物车、商品库存、订单主表、订单明细、异步通知、超时取消。只要中间有一步没处理好,数据就可能不一致。
所以这个项目在实现时,重点做了四件事:
- 用事务保证订单提交的一致性
- 用服务层复用订单取消逻辑
- 用 Redis 缓存优化商品读取
- 用 Celery 处理异步通知和延迟取消
二、订单提交为什么必须用事务
很多人一开始写订单接口时,会直接按顺序做下面几件事:
- 查询购物车
- 查询商品库存
- 创建订单
- 扣减库存
- 清空购物车
看起来没有问题,但如果执行到一半报错,比如订单创建成功了,库存却没扣成功,那系统数据就乱了。所以订单提交不能靠“按顺序执行”,而要靠事务把这一整组操作绑在一起。
这个项目里的核心入口就是 OrderSubmitAPIView:
class OrderSubmitAPIView(APIView):
def post(self, request): with transaction.atomic(): pass
虽然这里只展示了骨架,但它背后的核心思路很明确:
- 先锁定当前用户购物车
- 再锁定购物车项
- 再锁定即将扣减库存的商品
- 校验商品是否存在、是否上架、库存是否充足
- 创建订单主表和订单明细
- 批量扣减库存
- 删除购物车中已提交的商品
- 在事务提交后,再触发异步任务
这里真正关键的,不只是 transaction.atomic(),还有 select_for_update()。
它的作用可以简单理解成:在当前事务完成前,把相关行锁住,避免并发下单导致库存被重复扣减。
也就是说,这个项目在下单时考虑的不是“能不能提交订单”,而是“多个请求同时下单时还能不能保证数据正确”。
三、为什么异步任务要放到事务提交之后
订单提交成功后,项目还做了两件额外的事:
- 发送订单创建通知
- 安排一个延迟任务,用于超时自动取消订单
这里没有直接在事务内部调用任务,而是用了 transaction.on_commit(...)。相关骨架如下:
def enqueue_order_event_email(order_id, event): pass
def schedule_order_auto_cancel(order_id): pass
在订单提交流程里,调用方式大致是:
transaction.on_commit( lambda order_id=order.id: enqueue_order_event_email(order_id, "created") )
transaction.on_commit( lambda order_id=order.id: schedule_order_auto_cancel(order_id) )
为什么要这样写?因为如果事务还没真正提交,订单数据可能还没落库,这时 Celery worker 抢先执行,就可能查不到订单,或者拿到不完整的数据。
所以正确顺序应该是:
- 先让数据库事务完整提交
- 再去派发异步任务
这是一种非常典型、也非常实用的后端工程习惯。
四、订单取消为什么要抽到服务层
订单取消这件事,在这个项目里并不只会发生一次。
它至少有两个入口:
- 用户主动取消订单
- 系统检测到超时未支付,自动取消订单
如果把取消逻辑直接写死在视图里,那么自动取消就还得再写一遍,代码会重复,而且后续改规则时很容易漏。
所以项目把订单取消抽成了单独的服务函数:
def cancel_order(order, reason=""): pass
这个函数负责几件核心事情:
- 校验当前订单状态是否允许取消
- 锁定订单明细和关联商品
- 把订单商品数量加回库存
- 更新订单状态为 cancelled
- 如果有取消原因,则写入备注字段
这样一来,不管是手动取消还是系统自动取消,最终都走同一套逻辑。
这就是服务层存在的意义:把“业务规则”从“接口入口”里拆出来,提升复用性和可维护性。
对应的接口骨架如下:
class OrderCancelAPIView(APIView):
def post(self, request, order_id): pass
五、Celery 是怎么实现超时自动取消的
电商里一个很常见的业务需求就是:订单创建后,如果用户长时间未支付,就自动关闭订单并恢复库存。
这个项目没有用轮询脚本去扫库,而是采用了 Celery 延迟任务的方式。相关任务函数如下:
@shared_task def send_order_event_email(order_id, event): pass
@shared_task def auto_cancel_pending_order(order_id): pass
其中,auto_cancel_pending_order(order_id) 的思路是:
- 根据订单 ID 查询订单
- 判断订单当前是否还是 pending
- 根据 created_at + ORDER_AUTO_CANCEL_SECONDS 计算是否超时
- 如果超时,就调用 cancel_order(order, reason=...)
调度方式则是:
auto_cancel_pending_order.apply_async( args=[order_id], countdown=settings.ORDER_AUTO_CANCEL_SECONDS, )
这里的设计很巧妙。
它不是等系统每分钟来扫一次数据库,而是在订单创建的那一刻,就提前安排好一个“未来任务”。到了指定时间,Celery 自动执行;如果用户已经支付或者订单已取消,任务里再判断一次状态即可。
这种方式的优点很明显:
- 逻辑简单
- 与订单创建动作天然绑定
- 不需要额外写定时扫描逻辑
- 对小中型项目非常实用
六、商品缓存为什么不用“硬删 key”,而用版本号失效
商品列表、商品详情、分类列表这些接口,都是典型的高频读接口。如果每次都直接打数据库,后期访问量一上来,压力会很明显。
所以这个项目给商品和分类查询接入了缓存。相关函数骨架如下:
def build_category_cache_key(scope, query_string): pass
def build_product_list_cache_key(scope, query_string): pass
def build_product_detail_cache_key(scope, product_id): pass
def invalidate_category_cache(): pass def invalidate_product_cache(): pass
它的思路不是“更新一个商品时,手动删掉所有可能相关的 key”,因为商品列表的筛选条件很多,key 很分散,几乎删不干净。
这里采用的是“版本号失效”方案:
- 分类缓存维护一个版本号
- 商品缓存维护一个版本号
- 真正的缓存 key 由 版本号 + 查询条件 + scope 组合生成
- 商品或分类发生变更时,不直接删全部 key,而是把版本号加一
这样一来,旧 key 虽然还在 Redis 里,但已经没人再访问了;新请求会自动命中新版本 key。
这个方案在项目里还配合了 Django 信号完成自动失效:
@receiver(post_save, sender=Category) @receiver(post_delete, sender=Category)
def invalidate_category_related_cache(**kwargs): pass
@receiver(post_save, sender=Product)
@receiver(post_delete, sender=Product)
def invalidate_product_related_cache(**kwargs): pass
这套写法的优势在于:
- 缓存逻辑和业务查询逻辑解耦
- 不需要精确维护复杂 key 集合
- 商品修改后,缓存自动更新策略明确
- 对列表页和详情页都适用
七、测试为什么很重要
很多练手项目写到最后,接口能跑就结束了。但电商这种有状态业务,如果没有测试,后面一改功能很容易把已有逻辑带崩。
这个项目目前已经覆盖了 20 个测试用例,主要分布在三个模块里:
class UserAPIViewTests(TestCase): def test_register_success(self): pass
def test_login_returns_tokens(self): pass
def test_user_update_updates_profile(self): pass
class ProductAPIViewTests(TestCase):
def test_product_list_filters_for_anonymous_user(self): pass
def test_staff_can_see_inactive_product(self): pass
def test_product_list_cache_invalidates_after_product_update(self): pass
class OrderAPIViewTests(TestCase):
def test_cart_item_create_merges_existing_quantity(self): pass
def test_order_submit_creates_order_deducts_stock_and_clears_cart(self): pass
def test_order_cancel_restores_stock_and_marks_order_cancelled(self): pass
def test_auto_cancel_pending_order_restores_stock_after_timeout(self): pass
这些测试覆盖的不是“接口能不能返回 200”这么简单,而是项目里最关键的业务规则:
- 登录后能否正确拿到 Token
- 商品筛选逻辑是否正确
- 缓存是否在商品更新后失效
- 购物车重复添加是否会合并数量
- 下单后库存是否真的扣减
- 取消订单后库存是否恢复
- 超时自动取消任务是否真的生效
这类测试的价值,在于它验证的是“业务结果”,而不是“代码执行了没有”。
八、这个项目还有哪些可以继续优化
虽然这个项目已经比较完整,但从工程角度看,后面还可以继续扩展:
- 接入分页,避免商品列表一次返回过多数据
- 增加支付回调和支付状态流转
- 增加管理员商品管理和订单管理接口
- 增加接口限流和权限分层
- 增加 CI 流程,把测试自动化跑起来
- 增加部署方案,形成完整上线链路
也正因为这些扩展空间存在,这个项目很适合作为长期迭代的个人作品。
九、总结
如果说上篇介绍的是项目框架,那么下篇真正体现的是后端设计思维。
这个项目最值得总结的几点是:
- 订单提交必须放进事务里,不能分散执行
- 并发下单需要配合行级锁来保证库存一致性
- 异步任务要等事务提交后再派发
- 订单取消应该抽成服务层,避免逻辑重复
- 商品缓存适合用版本号失效,而不是手动删零散 key
- 电商项目的关键链路一定要有测试兜底
这也是我觉得 Django 电商项目真正有价值的地方。它不只是把接口写出来,而是在一步步逼近真实业务系统的处理方式。
更多推荐




所有评论(0)