
在每年大促与秒杀活动拉开帷幕的瞬间,网关层的瞬时流量往往会呈现数十倍乃至上百倍的陡峭脉冲。如果下单主链路依然采用传统的同步 RPC 模式,让前端请求强依赖于库存锁定、优惠券核销、积分累加以及物流履约等多个下游微服务,下游的关系型数据库连接池会在几秒钟内被彻底打爆,导致全链路级联雪崩。
因此,利用消息队列(MQ)进行异步削峰填谷是现代分布式系统的标准范式。前端在快速校验并生成基础订单后,立即将订单事件投递到 MQ 中并向用户返回“排队中”,下游履约服务按照自身集群的吞吐能力平稳拉取消费。
然而,将同步调用拆解为异步消息,直接引出了分布式架构中最险恶的两个核心挑战:
今天我们拆解如何利用 RocketMQ 事务消息(Half Message)与组合式幂等防御体系,构建一套坚不可摧的秒杀异步削峰落地方案。
一、RocketMQ 事务消息的两阶段提交机制
很多初学者容易写出这样的伪代码:
// 错误示范:无法保证原子性的伪代码
@Transactional
public void createOrder(OrderDTO dto) {
orderMapper.insert(order); // 1. 本地落库
rocketMQTemplate.send(topic, msg); // 2. 发送消息
}
这段逻辑存在巨大的漏洞:若 rocketMQTemplate.send() 在调用时遭遇网络抖动超时,抛出异常,会导致第 1 步的数据库事务回滚,但此时 MQ 消息可能已经在网络层面到达了 Broker 并被消费;反过来,如果发送成功后,在方法即将返回提交数据库事务的瞬间服务被强制 kill -9 重启,本地事务丢失而消息已然下发。
RocketMQ 采用两阶段提交(2PC)与事务回查机制彻底消除了这一破绽:
+———-+ +———-+ +———-+
| Producer | | Broker | | Consumer |
+———-+ +———-+ +———-+
| | |
| 1. 发送 Half 消息 (对消费者不可见) | |
|——————————->| |
| 2. Half 消息发送成功响应 | |
|<——————————-| |
| | |
| 3. 执行本地订单事务 (写本地DB) | |
| | |
| 4. 提交/回滚 (Commit / Rollback)| |
|——————————->| |
| | 5. 消息对消费者可见,投递消费 |
| |—————————->|
| | |
| [若步骤 4 超时或网络断连] | |
| 6. 定时发起事务状态回查 | |
|<——————————-| |
| 7. 检查本地订单表状态并返回响应 | |
|——————————->| |
- 若本地事务成功,向 Broker 发送 Commit 信号,Broker 将该消息恢复为真实 Topic 并投递到对应消费队列,下游开始处理;
- 若本地事务失败,向 Broker 发送 Rollback 信号,Broker 标记该消息为删除,永不消费。
二、生产级代码实现:事务监听器
在 Spring Boot 3.x / Java 24 环境下,我们利用 RocketMQLocalTransactionListener 落地事务监听逻辑:
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
private final OrderService orderService;
private final OrderMapper orderMapper;
public OrderTransactionListener(OrderService orderService, OrderMapper orderMapper) {
this.orderService = orderService;
this.orderMapper = orderMapper;
}
/**
* 执行本地事务:Half 消息发送成功后被回调
*/
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
OrderDTO orderDTO = (OrderDTO) arg;
// 真实落库:写入订单主表与防重流水
boolean success = orderService.createOrderInternal(orderDTO);
if (success) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
} catch (Exception e) {
// 发生未知异常,标记回滚,防止幽灵消息
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 事务状态回查:当 Broker 长时间未收到提交/回滚时主动探测
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = (String) msg.getHeaders().get("ORDER_ID");
if (orderId == null) {
return RocketMQLocalTransactionState.ROLLBACK;
}
// 仅需轻量查询订单主表,验证订单是否存在且状态合法
OrderStatus status = orderMapper.selectOrderStatus(orderId);
if (status != null && status != OrderStatus.FAILED) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
三、消费端的绝对幂等防重体系
消除了生产端的不一致,压力转移到了消费端。在每秒数万条订单履约消息涌入时,哪怕有 0.1% 的消息由于网络抖动被重试,没有幂等机制也会产生严重客诉。
很多团队喜欢单靠 Redis 的 SET resource_key my_random_value NX PX 30000 来做幂等,这在极端环境下是靠不住的:
- 如果 Redis 发生主从异步复制丢失或主节点宕机,分布式锁会失效;
- 如果业务处理时间意外超过了锁的 TTL 自动释放时间,后续的重试消息依然会并发闯入。
工业级的防重方案必须是双层漏斗:Redis 分布式锁作为前置抗压层,MySQL 唯一索引与幂等表作为兜底坚固层。
1. 幂等消费设计流程
[MQ 消息到达]
|
v
[步骤 1: Redis SETNX 拦截并发重复请求] —> (已被占用? 直接稍后重试或丢弃)
| (获取锁成功)
v
[步骤 2: 开启本地数据库事务]
|
v
[步骤 3: 尝试向消费流水表插入记录 (order_id + event_type 唯一索引)]
|—> (捕获 DuplicateKeyException: 说明已消费过,执行回滚并返回 SUCCESS)
|
v
[步骤 4: 执行核心履约扣减业务 (如核销优惠券、加积分)]
|
v
[步骤 5: 提交数据库事务,释放 Redis 锁,返回 MQ 消费成功 (CONSUME_SUCCESS)]
2. 消费端核心代码落地
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@RocketMQMessageListener(
topic = "ORDER_CREATE_TOPIC",
consumerGroup = "order_fulfillment_consumer_group"
)
public class OrderFulfillmentConsumer implements RocketMQListener<OrderCreatedEvent> {
private final RedisLockService redisLock;
private final TransactionTemplate transactionTemplate;
private final IdempotentRecordMapper recordMapper;
private final FulfillmentService fulfillmentService;
public OrderFulfillmentConsumer(RedisLockService redisLock,
TransactionTemplate transactionTemplate,
IdempotentRecordMapper recordMapper,
FulfillmentService fulfillmentService) {
this.redisLock = redisLock;
this.transactionTemplate = transactionTemplate;
this.recordMapper = recordMapper;
this.fulfillmentService = fulfillmentService;
}
@Override
public void onMessage(OrderCreatedEvent event) {
String lockKey = "lock:consume:" + event.orderId();
// 1. 前置拦截:基于 Redis 防短时间内的瞬时重复请求打爆 DB
if (!redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
// 拿不到锁说明已有线程正在处理该订单,直接抛异常让 MQ 稍后延时重试
throw new RuntimeException("Concurrent consuming, retry later for order: " + event.orderId());
}
try {
// 2. 核心事务闭环:通过唯一索引兜底
transactionTemplate.executeWithoutResult(status -> {
try {
// 利用数据库 (order_id, action_type) 的唯一键约束做原子防重
recordMapper.insertRecord(new IdempotentRecord(event.orderId(), "ORDER_FULFILL"));
} catch (DuplicateKeyException e) {
// 唯一索引冲突,确凿证明已经消费完毕,静默跳过,避免重复履约
return;
}
// 3. 执行真正的履约业务
fulfillmentService.fulfillOrder(event);
});
} finally {
redisLock.unlock(lockKey);
}
}
}
四、方案总结与架构演进反思
在秒杀订单削峰场景中,技术选型的本质是对“延迟”与“一致性”的精细权衡。
利用 RocketMQ 半消息守住“发的一侧”,利用唯一键幂等流水表守住“收的一侧”,再由 Redis 充当高并发防波堤。这套组合拳是业界应对大促海量脉冲流量最千锤百炼的工业基石。

