Administrator
Administrator
发布于 2026-03-23 / 1 阅读
0
0

拼团锁单接口性能优化

流程如下

  1. 基础参数校验

检查请求的核心字段(如 userId, source, channel, goodsId, activityId)是否为空。如果回调类型为HTTP,还会额外校验回调URL是否为空。参数不合法则直接返回错误。

  1. 幂等性校验(历史记录查询)

根据 userId 和外部交易单号 outTradeNo 查询是否已经存在未支付的订单。如果存在且状态为 CREATE(已创建),则直接返回该历史订单信息,防止重复锁单。

  1. 拼团目标达成拦截

如果请求中包含了 teamId(说明是参与已有拼团),系统会查询该团的当前进度。如果已锁定人数(lockCount)达到了目标人数(targetCount),则说明拼团已满,直接拦截本次锁单请求并返回目标已达成的提示。

  1. 营销优惠试算

调用 indexGroupBuyMarketService 根据用户、渠道、商品和活动信息进行预计算(试算),获取该笔交易的实际价格信息(如原价、抵扣价、最终支付价)以及活动详情。

  1. 人群与规则限定

根据上一步的试算结果 trialBalanceEntity,检查当前用户对该活动是否可见(isVisible)且活动是否可用(isEnable)。如果不满足条件,则拦截锁单。

  1. 执行核心锁单

准备锁单所需的三部分数据,调用底层的 tradeOrderService.lockMarketPayOrder 进行物理或逻辑层面的锁单(落库):

  • 用户信息userId
  • 活动信息:团队ID、活动ID、活动名称、开始/结束/有效时间、成团目标人数。
  • 支付与折扣信息:商品信息、原价、抵扣价、支付价、外部交易号,以及支付成功后的回调通知配置(MQ或HTTP)。
  1. 异常处理与结果返回
  • 成功:将生成的内部 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%

image-20260323224313812

总结

本次拼团锁单链路的改造,核心围绕**“无锁化”*与*“异步化”**展开。通过引入 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 在网络抖动或消费者重启时,极易发生消息重发。消费者端必须具备幂等处理能力。通过数据库表中的唯一索引(如 teamIdorderId),配合捕获 DuplicateKeyException 异常,遇到重复消息直接确认(ACK)丢弃,防止数据重复插入。
  • 异常重试与死信队列(DLQ): 若消费者在落库时遭遇数据库宕机或死锁,消息会不断重试。为防止“毒药消息(Poison Pill)”无限循环耗尽消费者线程,需配置明确的重试策略(如最大重试 3 次)。超过上限后,消息必须被路由至死信队列(Dead Letter Queue),随后触发人工告警或执行逆向库存回滚。

新的性能瓶颈分析

根据“系统压力守恒定律”,主链路(生产者)的压力释放,必然导致下游(消费者/基础设施)承压。在 535 QPS 的高压下,系统将暴露出新的瓶颈:

  1. 消费者吞吐量瓶颈(单条消费慢) 上游每秒打入 535 条消息,若消费者依然采用“单条拉取 -> 单行 Insert”的模式,由于频繁的数据库网络 I/O 建立,消费速度将远低于生产速度,导致 RabbitMQ 队列出现严重积压。 后续演进方向:需要将消费端改造为批次消费(Batch Pull),并在 MyBatis/DAO 层使用 批量插入(Batch Insert/Update),以数量换取 I/O 效率。
  2. 异步延迟引发的状态断层(结算强依赖) 异步落库削峰的代价是数据落地的延迟。若 MQ 消费过慢或发生轻微积压,前端用户在“锁单成功”后立刻跳转发起“结算”请求时,底层数据库中可能还查不到该订单的记录,直接导致结算流程阻断报错。 后续演进方向:前端需配合引入轮询或长连接等待机制;或在后置的结算校验环节,允许其穿透查询 Redis 中的订单中间态,实现读写链路的架构适配。

评论