异步下单之后,还需要补上哪些可靠性环节

旧博客的异步下单文章,把请求放进消息队列,再由消费者写订单。它很适合展示一个变化:用户请求不必一直等待完整的数据库操作。

这次再看代码,更值得继续讨论的是失败以后会发生什么。消息进了队列就一定有订单吗?库存已经更新,但订单插入失败怎么办?用户重复发一次请求,系统认得出来吗?

下面基于旧学习 demo 做一次代码阅读。它不是生产故障复盘,也没有新的压测结论。

先还原代码中的处理顺序

项目里可以沿 OrderControllerOrderMqReceiverOrderServiceImpl 追踪请求。

在异步订单方法中,代码依次校验库存、尝试乐观更新、删除库存缓存、写数据库订单,最后写入订单结果缓存。方法开头还有一个用于演示排队的十秒等待。

这说明它的重点是展示异步流程。实际工程中的排队应当从消息积压、消费者并发和任务耗时中观察,不能把演示用的等待当成处理策略。

请求受理和订单成功,需要两个结果

前端收到接口响应时,后台可能还没有开始处理订单。

可以让响应返回一个可查询的请求编号。客户端先展示“已受理”,再根据结果查询展示成功、库存不足或处理失败。

消息发布确认和消费者处理确认也要分开。RabbitMQ 的发布确认覆盖发布者到消息代理这一段,不能证明订单已经在业务数据库中创建。两种确认的边界可以参考 RabbitMQ 官方说明

乐观更新成功,不代表整个下单操作都成功

旧代码中,异步方法先更新库存,再插入订单。这个方法自身没有声明 @Transactional;同一个类里的悲观下单方法则明确声明了事务。

这是代码阅读发现的边界问题,不是已经验证的运行时故障。还应检查外层调用及事务配置,才能判断实际运行范围。

不过要设计这条链路,必须明确回答:库存更新成功而订单插入失败时,前者是否回滚?如果两者使用同一个数据库,通常可以把扣库存和建订单放在明确的本地事务里;缓存更新与结果通知还要设计提交后的处理和失败修复。

加了注解也要验证调用方式。Spring 默认代理模式下,同一对象内部的自调用不会经过事务代理,详见 Spring 事务文档

消费者捕获异常以后,谁来决定重试

OrderMqReceiver.process 捕获下单方法的异常后只记录日志,然后正常返回。

不能只看这一段就断言消息一定丢失,因为实际确认行为还取决于监听容器配置。但它提出了一个必须核对的问题:业务失败以后,框架是否还能感知失败,消息会重试、进入失败队列,还是被当作正常处理结束?

对可恢复错误可以设置有上限的重试;对库存不足这类业务终态,应记录明确结果;超过重试条件的消息应留下可查询、可处理的失败记录。具体确认时机需要与数据库提交顺序一起设计。

幂等需要落到稳定的业务标识

在入口查缓存,发现用户尚未下单,可以减少无效请求。但两个请求可能同时读到“尚未下单”,也可能因为消息重投而进入同一段消费逻辑。

因此,可以按业务规则选择请求编号、活动与用户组合等稳定标识,并通过数据库约束与事务内检查保障重复处理不会生成第二份业务结果。已经完成的请求再次到达时,应返回或查询已有结果。

缓存可以帮助查询和削减流量,但不应成为判断唯一业务结果的唯一依据。

结果查询也需要区分状态

缓存里查不到订单,可能表示尚未处理,也可能表示订单落库成功后缓存写入失败。只用一个布尔值,很难解释这些差异。

一个简化的结果模型可以包含 ACCEPTEDPROCESSINGSUCCEEDEDREJECTEDFAILED。这些是设计示例,并非旧项目已经实现的状态。状态应有明确的持久化位置、更新时间和查询路径;“没查到”与“已失败”需要分开处理。

下一次实验,优先验证失败路径

与其只比较接口平均耗时,更有价值的是构造几个确定的失败点:库存更新后订单插入失败;数据库提交后消费者进程退出;同一消息重复到达;订单成功后缓存不可用;乐观更新因并发竞争失败。

每次都检查数据库结果、消息状态和用户可查询的结果是否一致。性能实验则另外固定消费者数量、数据库连接池、请求规模与硬件条件。

异步化改变了等待的位置,也把可靠性问题分散到了多个步骤。把这些步骤逐个说清楚,旧 demo 就能继续成为理解工程问题的好材料。

代码来源:secKill,本次阅读基于提交 1f7d43a597ad;原系列:如何优雅地异步下订单

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2017-2026 Shadowalker
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信