
摘要今天来啃电商系统里最硬的一块骨头——订单系统。从状态机设计到下单事务从乐观锁到悲观锁一篇讲透。订单全流程待支付 → 已支付 → 已发货 → 已完成以及两条岔路——已取消和已退款。代码全部可运行。为什么订单系统是电商的心脏你想想一个电商系统商品展示可以丑一点搜索可以慢一点但订单要是出了问题——用户付了钱订单没创建钱没了货也没了。库存扣了订单没生成商品凭空蒸发。两个用户同时买最后一件库存变成 -1超卖了老板哭了。这不是 bug这是灾难。我在做商城的时候订单模块改了整整三版。第一版没有事务结果测试环境一压就出脏数据第二版用了悲观锁高并发直接卡死第三版才改成现在的乐观锁 状态机方案稳如老狗。所以订单系统的设计核心就三个词状态机、事务、锁。今天就把这三个词一个一个拆明白。原理速览状态机订单的交通规则状态机听起来很学术其实就是一个规则表订单当前是什么状态下一步能变成什么状态。就像交通规则——红灯只能变绿绿可以变黄但你不能从红灯直接变黄灯。订单也是一样你不能从待支付直接跳到已发货必须先经过已支付。事务要么全做要么全不做下单这一个动作其实包含了 5 个子操作扣减库存计算优惠创建订单创建订单项清理购物车这 5 步必须是一个原子操作——要么全部成功要么全部回滚。为什么必须原子看一个真实的回滚场景用户下单买 2 件衣服。第 1 步扣库存成功仓库 -2第 2 步计算优惠成功打 8 折第 3 步创建订单时数据库连接超时了——崩了。如果没有事务保护库存扣了但订单没建用户的 2 件衣服就这么凭空消失了。有了事务第 3 步失败会触发回滚第 1 步的库存扣减也会撤销。世界恢复原样用户可以重新下单。锁防止一货多卖库存只剩 1 件100 个人同时点下单——谁该买到两种策略策略乐观锁悲观锁核心思路先操作提交时检查有没有冲突先加锁操作完再释放数据库实现WHERE stock quantity条件更新SELECT ... FOR UPDATE行锁优点性能好不阻塞其他事务绝对不会超卖缺点冲突时需要重试高并发下排队等待适用场景日常购物冲突概率低秒杀、限时抢购冲突概率高一句话总结乐观锁是信任制悲观锁是门禁制。动手从状态机到下单事务状态机设计完整的状态转换图┌─────────┐ │ 待支付 │ (pending) └────┬────┘ ┌─────┴─────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已支付 │ │ 已取消 │ ← 终态 │ (paid) │ └─────────┘ └────┬─────┘ ┌─┴──────────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已发货 │ │ 已退款 │ ← 终态 │(shipped) │ └─────────┘ └────┬─────┘ ┌─┴──────────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 已完成 │ │ 已退款 │ ← 终态 └─────────┘ └─────────┘ (终态)用代码表示就是这张转换表当前状态允许转换的目标状态待支付 (pending)已支付 (paid)、已取消 (cancelled)已支付 (paid)已发货 (shipped)、已退款 (refunded)已发货 (shipped)已完成 (completed)、已退款 (refunded)已完成 (completed)无终态已取消 (cancelled)无终态已退款 (refunded)无终态代码实现非常简洁# 状态转换规则表ORDER_STATE_TRANSITIONS{pending:[paid,cancelled],paid:[shipped,refunded],shipped:[completed,refunded],completed:[],# 终态cancelled:[],# 终态refunded:[],# 终态}defcan_transition(from_status,to_status):检查状态转换是否合法allowedORDER_STATE_TRANSITIONS.get(from_status,[])returnto_statusinallowed执行转换的函数顺便记录时间节点核心逻辑deftransition_order(order,target_status):执行订单状态转换同时记录时间节点current_statusorder.status# 1. 校验转换合法性ifnotcan_transition(current_status,target_status):raiseValidationError(f非法转换{current_status}→{target_status})# 2. 更新状态order.statustarget_status# 3. 记录时间节点paid_at / shipped_at / completed_atnowtimezone.now()iftarget_statuspaid:order.paid_atnoweliftarget_statusshipped:order.shipped_atnoweliftarget_statuscompleted:order.completed_atnow order.save()returnorder状态机的好处把业务规则从代码里抽出来变成一张表。新人来了看这张表就知道订单怎么流转改规则也只改这一张表。这就是数据驱动的思想。下单事务5 步原子操作这是整个订单系统最关键的代码。核心框架关键片段fromdjango.dbimporttransactionfromdjango.db.modelsimportFdefcreate_order(user_id,cart_items,address_info,coupon_discountNone):创建订单 —— 整个流程在事务中执行任何一步失败全部回滚withtransaction.atomic():# ① 扣库存乐观锁条件更新affected 0 说明库存不足foritemincart_items:affectedGoods.objects.filter(iditem[goods_id],stock__gteitem[quantity],# 库存 购买数量).update(stockF(stock)-item[quantity]# F表达式数据库原子操作)ifaffected0:raiseValidationError(f商品{item[goods_id]}库存不足)# ② 计算总价 优惠优惠后不为负# ③ 创建订单生成订单号、地址快照、statuspending# ④ 批量插入订单项bulk_create 性能更好# ⑤ 清理购物车returnorder为什么要用transaction.atomic()想象一下如果没有事务第 1 步扣库存 ✅ 仓库 -2 第 2 步算优惠 ✅ 打 8 折 第 3 步建订单 ❌ 数据库连接超时了 第 4 步没执行 第 5 步没执行结果库存扣了优惠算了但订单没建出来。用户的库存没了订单也没了。这叫什么这叫事故。有了transaction.atomic()第 3 步失败后第 1 步的扣库存也会自动回滚。世界恢复原样用户可以重新下单。注意F(stock) - quantity的用法。这不是 Python 层面的减法而是让数据库自己算。F表达式会生成 SQL 的SET stock stock - N在数据库层面完成原子操作避免先读后写的竞态条件。乐观锁 vs 悲观锁来看两种锁的实际代码对比。乐观锁推荐日常使用# 核心WHERE stock quantity条件更新affectedGoods.objects.filter(idgoods_id,stock__gtequantity,# 条件检查).update(stockF(stock)-quantity# 数据库层面原子扣减)ifaffected0:raiseValidationError(库存不足)悲观锁秒杀场景# 核心select_for_update() 加行锁goodsGoods.objects.select_for_update().get(idgoods_id)ifgoods.stockquantity:raiseValidationError(库存不足)goods.stock-quantity goods.save()两者区别在哪乐观锁像信任制——我先改提交时检查有没有被别人改过。没冲突直接成功有冲突就重试。SQL 是UPDATE goods SET stock stock - 1 WHERE id 1 AND stock 1一句话说完。悲观锁像门禁制——我先锁门改完了再开门。别人要改同一件商品排队等着。SQL 是SELECT ... FOR UPDATE先锁住这一行其他事务读取同一行会被阻塞。怎么选看场景日常购物同时买同一件商品的概率不高乐观锁够用性能好。秒杀活动几千人同时抢 10 件商品冲突概率极高必须悲观锁。商城的策略普通商品用乐观锁秒杀商品用悲观锁。既保证性能又保证安全。订单查询与接口设计商城规范所有订单操作统一传数字主键 id。classOrderViewSet(viewsets.ModelViewSet):订单接口permission_classes[IsAuthenticated]defget_queryset(self):只返回当前用户的订单returnOrder.objects.filter(userself.request.user)# GET /api/orders/ → 订单列表# GET /api/orders/{id}/ → 订单详情传数字主键 id# POST /api/orders/ → 创建订单下单# POST /api/orders/{id}/cancel/ → 取消订单# POST /api/orders/{id}/complete/ → 确认收货# POST /api/orders/{id}/refund/ → 申请退款为什么用id而不是order_noorder_no是给用户看的用于客服沟通、物流查询。数字主键id是数据库层面的唯一标识查询效率最高主键自带索引。统一用id前后端对接更简洁。如果有的接口传id、有的传order_no前后端同学不得打起来取消订单 退款取消和退款不只是改个状态——还得把库存还回去。核心思路action(detailTrue,methods[post],url_pathcancel)defcancel(self,request,pkNone):取消订单 回滚库存orderself.get_object()# 1. 状态机校验只有 pending → cancelled 是合法的transition_order(order,cancelled)# 2. 回滚库存遍历订单项把扣的库存加回去用 F(stock) quantity# 这一步和改状态必须在同一个事务里returnResponse({code:0,message:订单已取消})退款逻辑类似paid → refunded或shipped → refunded库存同样要回滚。注意取消和退款的库存回滚也应该放在事务里。如果改完状态、回滚库存到一半崩了就会出现订单取消了但库存没还的惨剧。用 AI 生成状态机代码状态机这种东西特别适合让 AI 来写。因为模式非常固定定义状态常量定义转换规则表写校验函数写执行函数这四步AI 几秒钟就能搞定而且质量很高。prompt 是这样的你是一个 Django 开发者。请为电商订单系统实现一个状态机。 状态列表pending、paid、shipped、completed、cancelled、refunded 要求 1. 用字典定义状态转换规则 2. 实现 can_transition() 校验函数 3. 实现 transition_order() 执行转换同时记录时间节点 4. 所有非法转换抛出 ValidationError 5. 代码风格符合 PEP 8带中文注释AI 给出的代码和我们的实现几乎一致甚至还会额外加日志记录和状态标签映射。不过有一点要注意AI 生成的事务代码你一定要亲自检查。因为 AI 不知道你项目的具体业务逻辑——比如优惠券过期了要不要回滚、“部分退款怎么算”这些都需要人来把关。AI 加速心得状态机、校验逻辑、序列化器这类规则明确、模式固定的代码放心交给 AI。但涉及业务判断、事务边界的代码AI 只是助手你才是决策者。学完来练练手加一个超时自动取消订单创建后 30 分钟未支付自动变成已取消、库存回滚。提示Celery Beat 定时任务或 Django Signals加一个部分退款订单里 3 件商品用户只退其中 1 件。库存怎么回滚金额怎么算状态怎么变提示可能需要新增一个partial_refunded状态加一个订单状态变更通知每次状态变更时给用户发推送通知。提示Django Signal 的post_save检测status字段变化给乐观锁加重试机制当条件更新返回affected 0时不直接报错等 50ms 后重试最多 3 次。提示time.sleep(0.05)for循环做完你会发现一个真实的订单系统远不止今天这些内容。但核心框架——状态机 事务 锁——已经搭好了后面都是在这个基础上长肉。配套文件day10_order.py状态机 下单事务 乐观锁/悲观锁双版本 DRF 接口可参考学习。本系列为梅雅达编程笔记原创首发于 CSDN。有问题评论区见