流程如下
- 基础参数校验
检查请求的核心字段(如 userId, source, channel, goodsId, activityId)是否为空。如果回调类型为HTTP,还会额外校验回调URL是否为空。参数不合法则直接返回错误。
- 幂等性校验(历史记录查询)
根据 userId 和外部交易单号 outTradeNo 查询是否已经存在未支付的订单。如果存在且状态为 CREATE(已创建),则直接返回该历史订单信息,防止重复锁单。
- 拼团目标达成拦截
如果请求中包含了 teamId(说明是参与已有拼团),系统会查询该团的当前进度。如果已锁定人数(lockCount)达到了目标人数(targetCount),则说明拼团已满,直接拦截本次锁单请求并返回目标已达成的提示。
- 营销优惠试算
调用 indexGroupBuyMarketService 根据用户、渠道、商品和活动信息进行预计算(试算),获取该笔交易的实际价格信息(如原价、抵扣价、最终支付价)以及活动详情。
- 人群与规则限定
根据上一步的试算结果 trialBalanceEntity,检查当前用户对该活动是否可见(isVisible)且活动是否可用(isEnable)。如果不满足条件,则拦截锁单。
- 执行核心锁单
准备锁单所需的三部分数据,调用底层的 tradeOrderService.lockMarketPayOrder 进行物理或逻辑层面的锁单(落库):
- 用户信息:
userId - 活动信息:团队ID、活动ID、活动名称、开始/结束/有效时间、成团目标人数。
- 支付与折扣信息:商品信息、原价、抵扣价、支付价、外部交易号,以及支付成功后的回调通知配置(MQ或HTTP)。
- 异常处理与结果返回
- 成功:将生成的内部
orderId、订单状态以及各项价格、teamId封装返回。 - 异常捕获:分别捕获业务异常
AppException和系统未知异常Exception,记录日志并返回对应的错误码。
耗时分析
各阶段耗时如下(无锁化库存扣减)
| 流程阶段 | 第一次请求 (开团 / Leader) | 第二次请求 (参团 / MemberA) |
|---|---|---|
| 阶段1 (参数校验) | 0ms | 0ms |
| 阶段2 (历史订单查询) | 3ms | 2ms |
| 阶段3 (拼团进度查询) | 1ms | 1ms |
| 阶段4 (优惠试算与人群限定) | 5ms | 5ms |
| 阶段5 (底层核心锁单) | 19ms | 20ms |
| 锁单总耗时 | 28ms | 28ms |
主要耗时就集中在锁单这里 20ms左右
26-03-23.20:58:11.166 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 7ms
26-03-23.20:58:11.179 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(聚合订单落库): 13ms
26-03-23.20:58:11.179 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 20ms
锁单中落库又花费了13ms
改造
Lua脚本封装命令
使用Lua脚本对原来的进行改造,责任链的耗时下降了3ms左右,这里主要是原来的无锁化库存扣减需要至少4个命令,也就是4个RTT,而用Lua脚本代替,就是一个RTT了
26-03-23.21:26:07.846 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 3ms
26-03-23.21:26:07.858 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(聚合订单落库): 12ms
26-03-23.21:26:07.858 [http-nio-8091-exec-2] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 15ms
26-03-23.21:27:46.712 [http-nio-8091-exec-8] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 4ms
26-03-23.21:27:46.727 [http-nio-8091-exec-8] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(聚合订单落库): 15ms
26-03-23.21:27:46.727 [http-nio-8091-exec-8] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 19ms
26-03-23.21:27:46.681 [http-nio-8091-exec-3] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 3ms
26-03-23.21:27:46.697 [http-nio-8091-exec-3] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(聚合订单落库): 16ms
26-03-23.21:27:46.697 [http-nio-8091-exec-3] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 19ms
MQ异步落盘
想要进一步优化就是需要将这个落库进行压缩,首先想到的就是MQ,然后异步刷盘
26-03-23.22:37:07.070 [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] INFO LockOrderTopicListener - 接收消息(异步锁单落库)- 开始执行数据库落库操作:{"groupBuyOrderAggregate":{"payActivityEntity":{"activityId":100123,"activityName":"测试活动","endTime":1891304380000,"startTime":1733537980000,"targetCount":100000,"teamId":"swC8gE4m","validTime":15},"payDiscountEntity":{"channel":"c01","deductionPrice":20.00,"goodsId":"9890001","goodsName":"《手写MyBatis:渐进式源码实践》","notifyConfigVO":{"notifyType":"HTTP","notifyUrl":"http://127.0.0.1:8091/api/v1/test/group_buy_notify"},"originalPrice":100.00,"outTradeNo":"TD_9699e308-8dcc-4995-a5cb-ff3859def5c6","payPrice":80.00,"source":"s01"},"userEntity":{"userId":"user_leader_1_1774276626957"},"userTakeOrderCount":0},"orderId":"197338774822","teamId":"swC8gE4m"}
26-03-23.22:37:14.403 [http-nio-8091-exec-4] INFO MarketTradeController - 营销交易锁单:user_leader_1_1774276634400 LockMarketPayOrderRequestDTO:{"activityId":100123,"channel":"c01","goodsId":"9890001","notifyConfigVO":{"notifyType":"HTTP","notifyUrl":"http://127.0.0.1:8091/api/v1/test/group_buy_notify"},"outTradeNo":"TD_de41696b-41e9-4477-a9c2-0c75311c1018","source":"s01","teamId":"","userId":"user_leader_1_1774276634400"}
26-03-23.22:37:14.403 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 阶段1(参数校验): 0ms
26-03-23.22:37:14.403 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 拼团交易-查询未支付营销订单:user_leader_1_1774276634400 outTradeNo:TD_de41696b-41e9-4477-a9c2-0c75311c1018
26-03-23.22:37:14.406 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 阶段2(历史订单查询): 3ms
26-03-23.22:37:14.406 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 拼团交易-查询拼单进度:
26-03-23.22:37:14.408 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 阶段3(拼团进度查询): 2ms
26-03-23.22:37:14.408 [http-nio-8091-exec-4] INFO RootNode - 拼团商品查询试算服务-RootNode userId:user_leader_1_1774276634400 requestParameter:{"activityId":100123,"channel":"c01","goodsId":"9890001","source":"s01","userId":"user_leader_1_1774276634400"}
26-03-23.22:37:14.410 [pool-2-thread-3 ] INFO AbstractRepository - 缓存命中:group_buy_market_cn.dztyykxx.infrastructure.dao.po.GroupBuyActivity_100123
26-03-23.22:37:14.412 [pool-2-thread-3 ] INFO AbstractRepository - 缓存命中:group_buy_market_cn.dztyykxx.infrastructure.dao.po.GroupBuyDiscount_25120207
26-03-23.22:37:14.412 [http-nio-8091-exec-4] INFO MarketNode - 拼团商品试算服务, userId:user_leader_1_1774276634400, requestParameter:MarketProductEntity(userId=user_leader_1_1774276634400, activityId=100123, goodsId=9890001, source=s01, channel=c01)
26-03-23.22:37:14.413 [http-nio-8091-exec-4] INFO TagNode - [TagNode]用户是否在该标签中:false
26-03-23.22:37:14.413 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 阶段4(优惠试算与人群限定): 5ms
26-03-23.22:37:14.413 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 拼团交易-锁定营销优惠支付订单:user_leader_1_1774276634400 activityId:100123 goodsId:9890001
26-03-23.22:37:14.413 [http-nio-8091-exec-4] INFO ActivityUsabilityRuleFilter - 交易规则过滤-活动可用性校验user_leader_1_1774276634400 activityId:100123
26-03-23.22:37:14.415 [http-nio-8091-exec-4] INFO UserTakeLimitRuleFilter - 交易规则过滤-次数校验user_leader_1_1774276634400 activityId:100123
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO TeamStockOccupyRuleFilter - 交易规则过滤-组队库存校验user_leader_1_1774276634400 activityId:100123
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 3ms
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(MQ投递): 0ms
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 3ms
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 阶段5(底层核心锁单): 3ms
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO MarketTradeController - 交易锁单记录(新):user_leader_1_1774276634400 marketPayOrderEntity:{"deductionPrice":20.00,"orderId":"489574779264","originalPrice":100.00,"payPrice":80.00,"teamId":"3CCB1OtS","tradeOrderStatusEnumVO":"CREATE"}
26-03-23.22:37:14.416 [http-nio-8091-exec-4] INFO MarketTradeController - 耗时统计 - 锁单总耗时(成功): 13ms
这时候一个锁单的完整流程就只有13ms了
其中库存扣减只有3毫秒
| 流程阶段 | 优化前 (预热后) | 优化后 (当前) | 耗时分析 |
|---|---|---|---|
| 阶段1 (参数校验) | 0ms | 0ms | 内存操作,无变化。 |
| 阶段2 (历史订单查询) | 3ms | 3ms | 涉及单次 DB 查询,耗时持平。 |
| 阶段3 (拼团进度查询) | 1ms | 2ms | 耗时基本持平(网络波动带来的极小误差)。 |
| 阶段4 (试算与人群限定) | 5ms | 5ms | 纯内存规则树计算与缓存读取,耗时稳定。 |
| 阶段5 (底层核心锁单) | ~20ms | 3ms | 性能核心突破点。下降显著。 |
| 锁单总耗时 | 28ms | 13ms | API 响应耗时缩短了 53%。 |
库存扣减中是
26-03-23.22:52:27.841 [http-nio-8091-exec-292] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段1(责任链规则过滤): 3ms
26-03-23.22:52:27.841 [http-nio-8091-exec-292] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单阶段2(MQ投递): 0ms
26-03-23.22:52:27.841 [http-nio-8091-exec-292] INFO TradeLockOrderServiceImpl - 耗时统计 - 底层锁单总耗时(成功): 3ms
| 流程阶段 | 优化前 (同步 DB) | 优化后 (Lua + MQ) | 耗时变化 | 优化原理剖析 |
|---|---|---|---|---|
| 阶段 1 (责任链与库存扣减) | 7ms | 3ms | -57% | 将 4 次 Redis 网络交互(Get, Incr, 判断, SetNx)压缩为 1 次 Lua 脚本执行,大幅降低了网络往返延迟 (RTT)。 |
| 阶段 2 (持久化操作) | 13ms*(聚合订单落库)* | 0ms*(MQ消息投递)* | -100% | 将耗时的 MySQL 磁盘 I/O 及事务等待剥离,改为仅在内存构建消息并投递至 RabbitMQ,主线程几乎 0 耗时。 |
| 底层锁单总耗时 | 20ms | 3ms | -85% | 核心链路全面去除 I/O 阻塞,最大化释放 Web 线程池资源。 |
此时再进行全链路压测,QPS能到535,相比之前同步刷盘的QPS只有330,提升了80%

总结
本次拼团锁单链路的改造,核心围绕**“无锁化”*与*“异步化”**展开。通过引入 Lua 脚本将 Redis 库存扣减操作(GET、INCR、校验、SETNX)合并为单次网络交互,并剥离耗时的 MySQL 事务,将其交由 RabbitMQ 异步落库。这一改造将核心锁单操作耗时从 20ms 压缩至 3ms 内,使 API 接口的整体 QPS 从 330 跃升至 535,吞吐量提升约 62%。
但在系统吞吐量取得突破的同时,异步架构不可避免地牺牲了强一致性,并为系统引入了新的复杂度和潜在瓶颈。
新问题
将数据库的同步刷盘改为 MQ 异步投递后,系统面临的主要挑战集中在数据一致性与异常处理:
- MQ 投递失败与“少卖”补偿: 当前流程为“先扣 Redis 库存,再投递 MQ”。若 MQ 投递失败(如网络中断),会导致 Redis 库存已扣但订单未生成,引发“少卖”。
- 实时处理:在投递异常的
catch块中,需向外抛出异常,触发业务层的兜底逻辑,对 Redis 中的恢复量(Recovery Count)进行加 1 操作,释放占用资格。 - 最终一致性兜底:针对服务发版或极端宕机导致内存补偿失败的场景,必须引入独立的定时任务(Worker),定期比对 Redis 缓存库存与 MySQL 实际落库数量的差异,进行数据对账与修复。
- 实时处理:在投递异常的
- 重复消费与幂等性设计: MQ 在网络抖动或消费者重启时,极易发生消息重发。消费者端必须具备幂等处理能力。通过数据库表中的唯一索引(如
teamId和orderId),配合捕获DuplicateKeyException异常,遇到重复消息直接确认(ACK)丢弃,防止数据重复插入。 - 异常重试与死信队列(DLQ): 若消费者在落库时遭遇数据库宕机或死锁,消息会不断重试。为防止“毒药消息(Poison Pill)”无限循环耗尽消费者线程,需配置明确的重试策略(如最大重试 3 次)。超过上限后,消息必须被路由至死信队列(Dead Letter Queue),随后触发人工告警或执行逆向库存回滚。
新的性能瓶颈分析
根据“系统压力守恒定律”,主链路(生产者)的压力释放,必然导致下游(消费者/基础设施)承压。在 535 QPS 的高压下,系统将暴露出新的瓶颈:
- 消费者吞吐量瓶颈(单条消费慢) 上游每秒打入 535 条消息,若消费者依然采用“单条拉取 -> 单行 Insert”的模式,由于频繁的数据库网络 I/O 建立,消费速度将远低于生产速度,导致 RabbitMQ 队列出现严重积压。 后续演进方向:需要将消费端改造为批次消费(Batch Pull),并在 MyBatis/DAO 层使用 批量插入(Batch Insert/Update),以数量换取 I/O 效率。
- 异步延迟引发的状态断层(结算强依赖) 异步落库削峰的代价是数据落地的延迟。若 MQ 消费过慢或发生轻微积压,前端用户在“锁单成功”后立刻跳转发起“结算”请求时,底层数据库中可能还查不到该订单的记录,直接导致结算流程阻断报错。 后续演进方向:前端需配合引入轮询或长连接等待机制;或在后置的结算校验环节,允许其穿透查询 Redis 中的订单中间态,实现读写链路的架构适配。