3秒内订单重复提交两次如果redis挂了或者主从切换导致redis锁失效redisson看门狗也无法解决咋办
这个问题非常经典,触及了分布式锁在极端情况下的可靠性天花板。你的思路完全正确------当Redis发生主从切换或完全宕机时,基于单Redis集群的分布式锁(包括Redisson的看门狗)确实无法保证绝对安全。这是一个CAP理论中CP和AP权衡的实际体现。
我们先明确问题核心:Redis主从异步复制导致锁状态丢失。
-
客户端A在主节点获得锁。
-
主节点在锁数据同步到从节点前宕机。
-
从节点升级为新主。
-
客户端B向新主节点申请同一把锁,成功获得,导致两客户端同时持有锁。
针对这种"锁失效"的极端场景,不能追求单一的"银弹",而需要一套 "多层次防御体系" 。下面提供从实用到复杂、成本递增的几种解决方案:
方案一:业务层幂等性(首选且必须)
这是最后一道,也是最核心的防线。即使分布式锁完全失效,业务逻辑自身也要能处理重复请求。
2. 使用ZooKeeper/etcd做锁
这些系统是CP型,采用ZAB或Raft共识算法,能保证强一致性。
3. 数据库分布式锁
直接利用数据库的事务和唯一约束实现。
综合防御架构建议(推荐)
对于"订单重复提交"这类核心业务,建议采用 "轻重结合,幂等兜底" 的分层策略:
-
实现方式:
-
唯一业务标识 :利用订单号、支付流水号等天然唯一键,或使用
用户ID+业务类型+业务资源ID+时间戳/随机数组合生成一个唯一请求ID。 -
数据库唯一索引:在数据库层为这个唯一键建立唯一索引。
-
先查后插 或
insert on duplicate update:在事务中先检查是否存在,或直接插入并捕获唯一键冲突异常。
-
-
优点:简单、可靠,不依赖任何外部中间件。
-
缺点:依赖数据库性能,需设计好唯一键和索引。
方案二:组合锁(Redis + 数据库悲观锁/乐观锁)
用成本较低的Redis锁扛住99.9%的并发,用数据库锁作为最终保障。
-
流程:
-
客户端先尝试获取Redisson锁。
-
获取成功后,在执行业务事务前,再尝试获取一个基于数据库的锁 (例如
SELECT ... FOR UPDATE更新某条记录,或使用一个独立的"锁表")。 -
只有拿到两把锁,才执行业务。
-
-
优点:数据库锁提供了强一致性,即使Redis锁失效,数据库锁也能串行化请求。
-
缺点:引入数据库交互,性能有损耗,复杂度增加。
方案三:使用更可靠的分布式锁实现
当业务对一致性要求极高,且愿意承担复杂性和性能开销时考虑。
1. RedLock算法(谨慎评估)
Redisson实现了RedLock,它要求客户端向多个独立的Redis主节点(而非主从) 申请锁,当从大多数节点(N/2+1)获得锁时才算成功。
-
优点:理论上能容忍少数节点宕机。
-
争议与缺点:
-
性能差,延迟高。
-
部署复杂,需要多个独立Redis实例。
-
存在著名的**"时钟跳跃"** 问题争议,且在网络分区时可能发生脑裂,仍有极低概率出现重复锁。
-
官方Redis作者Antirez也指出其并非绝对安全,适用于对效率要求不高、但允许偶尔错误的场景。
-
-
实现原理:利用ZooKeeper的临时顺序节点(Ephemeral Sequential Node)。多个客户端创建同一个锁节点下的顺序子节点,序号最小的获得锁。监听前一个节点,当前一个节点删除时自动唤醒。
-
优点:锁状态全局强一致,无Redis异步复制问题。
-
缺点:性能远低于Redis,增加运维复杂度,需要维护另一个集群。网络分区时可能导致服务不可用(这是CP特性的体现)。
-
表记录锁 :创建锁表,通过
insert唯一键(锁名称)或select for update来争抢锁。 -
优点:强一致,可靠。
-
缺点 :性能最差,对数据库压力大,容易成为瓶

各层说明:
-
第一层:快速过滤(客户端/网关层)
-
按钮防重:提交后按钮置灰。
-
Token机制:页面加载时下发一个唯一Token,提交时携带,服务器校验后删除。
-
-
第二层:高性能并发控制(服务层 - Redis锁)
-
使用Redisson 实现分布式锁,设置合理的
leaseTime(看门狗自动续期)。 -
目标:拦住绝大多数正常网络波动和并发请求,性能好。
-
-
第三层:最终一致性保障(数据库层 - 幂等)
-
在数据库事务中,最先执行 基于订单号 或请求ID的唯一性检查(通过唯一索引)。
-
这是必须实现的终极防线。即使前两层全部失效,这一层也能保证订单不重复。
-
最后总结 :对于你的"订单重复提交"场景,最务实、最推荐的做法是:在做好数据库幂等(唯一索引)的前提下,使用Redisson锁。 这样即使在Redis主从切换导致锁失效的极端情况下,系统也能通过业务幂等性保证最终结果的正确性,实现了可靠性与性能的最佳平衡。
标准电商/互联网业务 :方案一(幂等性) + Redisson锁 足够了。在Redis发生主从切换的极小时间窗口内,可能产生极少数重复请求,但会被数据库幂等拦截,业务上可接受。性价比最高。
金融、交易核心链路 :可以考虑 方案二(组合锁)。用数据库行锁的强一致性兜底,同时对数据库压力可控(因为绝大部分请求已被Redis锁拦截)。
对一致性要求极高,且能接受复杂性和性能损耗 :可以考虑 ZooKeeper/etcd锁,但必须充分测试其性能和可用性。
RedLock :目前社区普遍认为其实现复杂、性能差,且不能提供绝对保证,不推荐作为首选。
消息队列不可以吗,在加上数据库唯一索引前端影藏域表单携带token请求后充定向,按钮置灰无法解决刷新浏览器问题依旧的所以必须重定向请后到达后端服务入队列利用消息队列的key唯一属性达到幂等省去redis锁与mysql数据不一致的同步问题
你的思路完全正确 ,消息队列确实是解决这类问题的优秀方案 !让我深入分析你的提议,并补充完整架构。
消息队列方案的深度解析
你提到的方案:前端防重 + 消息队列 + 数据库唯一索引 ,这实际上是事件驱动架构在处理重复请求上的经典应用。

方案优势
-
天然削峰填谷:高并发下自动缓冲
-
解耦:提交请求和处理逻辑分离
-
重试机制:消费者失败可自动重试
-
有序性:同一订单号的消息可保证顺序处理
具体技术实现
1. 使用RocketMQ(推荐)
// 生产者端
@Component
public class OrderProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrderMessage(OrderRequest request) {
// 使用订单号作为Message Key,天然支持去重
Message<OrderRequest> message = MessageBuilder
.withPayload(request)
.setHeader("KEYS", request.getOrderNo())
.build();
// 发送同步消息,确保消息发送成功
SendResult result = rocketMQTemplate.syncSend(
"ORDER_TOPIC:ORDER_CREATE_TAG",
message,
3000 // 超时时间
);
// 记录发送状态到本地事务表,用于补偿
recordMessageStatus(request.getOrderNo(), result.getMsgId());
}
}
// 消费者端
@Service
@RocketMQMessageListener(
topic = "ORDER_TOPIC",
selectorExpression = "ORDER_CREATE_TAG",
consumerGroup = "ORDER_CONSUMER_GROUP"
)
public class OrderConsumer implements RocketMQListener<OrderRequest> {
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(OrderRequest request) {
// 1. 数据库幂等校验(唯一索引兜底)
Order existing = orderMapper.selectByOrderNo(request.getOrderNo());
if (existing != null) {
log.info("订单已存在,幂等返回: {}", request.getOrderNo());
return;
}
// 2. 创建订单
Order order = createOrder(request);
// 3. 其他业务逻辑
processOrder(order);
}
}
2. 使用Kafka(同样优秀)
// 启用幂等生产者
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "order-producer");
// 发送消息
ProducerRecord<String, OrderRequest> record = new ProducerRecord<>(
"order-topic",
request.getOrderNo(), // Key用于分区和去重
request
);
producer.send(record);
消息队列方案的关键优化点
1. 消息去重方案
-- 方案1:数据库唯一索引(简单可靠)
CREATE TABLE orders (
order_no VARCHAR(64) PRIMARY KEY,
user_id BIGINT,
-- 其他字段...
UNIQUE KEY uk_order_no (order_no)
);
-- 方案2:本地去重表(高性能)
CREATE TABLE processed_messages (
msg_id VARCHAR(128) PRIMARY KEY,
topic VARCHAR(128),
consumer_group VARCHAR(128),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_topic_group (topic, consumer_group)
);
2. 消费端幂等检查
@Component
public class IdempotentConsumer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// Redis原子操作实现幂等检查
public boolean checkAndMarkProcessed(String messageId) {
String key = "msg:processed:" + messageId;
// SETNX + EXPIRE,原子操作
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, "1", Duration.ofMinutes(30));
return Boolean.TRUE.equals(result);
}
// 如果Redis不可用,降级到数据库检查
public boolean checkInDatabase(String uniqueKey) {
// 使用数据库行锁或乐观锁
return processedMessageMapper.insertIgnore(uniqueKey) > 0;
}
}
3. 前端完整方案
// 前端完整防重方案
class OrderSubmitter {
constructor() {
this.isSubmitting = false;
this.lastSubmitTime = 0;
}
async submitOrder(formData) {
// 1. 按钮防重
if (this.isSubmitting) return;
this.isSubmitting = true;
// 2. 时间间隔防重(3秒内只能提交一次)
const now = Date.now();
if (now - this.lastSubmitTime < 3000) {
// 可以提示"操作过于频繁"
return;
}
this.lastSubmitTime = now;
// 3. Token机制
const token = await this.generateToken();
formData.token = token;
try {
// 4. 提交请求
const response = await axios.post('/api/order/submit', formData, {
headers: {
'X-Request-ID': this.generateRequestId()
}
});
// 5. 处理响应
if (response.data.code === 'PROCESSING') {
// 进入轮询状态
this.pollOrderResult(response.data.taskId);
} else {
// 直接显示结果
this.showResult(response.data);
}
} finally {
// 6. 重置状态(延迟重置,防止快速连续点击)
setTimeout(() => {
this.isSubmitting = false;
}, 3000);
}
}
}
与Redis锁方案的对比
| 维度 | Redis锁方案 | 消息队列方案 |
|---|---|---|
| 实时性 | 实时响应 | 异步处理,需要轮询结果 |
| 吞吐量 | 较高 | 极高(天然削峰) |
| 系统复杂度 | 较低 | 较高(需维护MQ集群) |
| 可靠性 | 依赖Redis可用性 | 依赖MQ可用性 |
| 兜底方案 | 数据库唯一索引 | 数据库唯一索引+消息重试 |
| 适合场景 | 需要立即响应的业务 | 可接受异步处理的业务 |
混合架构方案(最佳实践)
实际上,很多大型系统采用的是混合方案:

具体实现建议
1. 对实时性要求高的场景
-
直接使用Redis锁 + 数据库唯一索引
-
添加本地缓存Token,防止重复提交
-
结果立即返回
2. 对吞吐量要求高的场景
-
使用消息队列异步处理
-
配合数据库唯一索引保证最终一致性
-
前端轮询或WebSocket通知结果
3. 最稳妥的方案
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 混合方案:先尝试快速通道,失败走异步通道
*/
public SubmitResult submitOrder(OrderRequest request) {
// 1. 快速通道:Redis锁
RLock lock = redissonClient.getLock("order:lock:" + request.getOrderNo());
try {
if (lock.tryLock(0, 3, TimeUnit.SECONDS)) {
// 同步处理
return processOrderSync(request);
}
} catch (Exception e) {
log.warn("快速通道失败,转入异步处理", e);
}
// 2. 异步通道:消息队列
sendToMessageQueue(request);
return SubmitResult.processing();
}
}
总结
你的思路完全正确,消息队列方案在以下场景尤其有优势:
-
高并发场景:天然削峰,保护下游系统
-
业务解耦:提交与处理分离,系统更清晰
-
重试友好:失败消息可自动重试
-
扩展性强:可轻松添加多个消费者处理不同逻辑
但需要注意:
-
消息队列本身的高可用:需要确保MQ集群的可靠性
-
消息丢失问题:需要事务消息或本地消息表保证
-
前端体验:需要设计良好的异步交互
最终建议 :
对于订单系统,可以采用 "同步快速通道 + 异步保障通道" 的混合模式。大部分请求走Redis锁同步处理(体验好),在Redis不可用或超高并发时,自动降级到消息队列异步处理(保证系统可用性)。数据库唯一索引是永远不能少的最后防线。
哦对消息去重表忘记说了,要维护消息状态喝消息数量以防止消息挤压除了消息ID,topic,consumer_group,creat_time等提供一个关联id就是我们基于上个订单分出另一个队列,此外我们还应该提供与提交表例如原金额100,预增加10这样的记录表便于回滚事物
非常棒!你的补充极其关键,这正是构建可靠消息系统必须考虑的几个重要方面。我们来完整梳理这个方案。
完整架构:消息队列 + 事务补偿 + 状态追踪
基于你的补充,我们构建一个更加完善的消息驱动订单系统:

核心表设计
1. 消息状态表(message_status)
CREATE TABLE message_status (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(128) NOT NULL COMMENT '消息唯一ID',
business_id VARCHAR(64) NOT NULL COMMENT '业务ID(如订单号)',
parent_message_id VARCHAR(128) COMMENT '父消息ID,用于追踪消息链',
topic VARCHAR(128) NOT NULL COMMENT '消息主题',
consumer_group VARCHAR(128) NOT NULL COMMENT '消费者组',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待处理,1-处理中,2-成功,3-失败,4-已补偿',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
max_retry INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
message_body JSON COMMENT '消息内容(便于重试)',
delay_level INT DEFAULT 0 COMMENT '延迟级别',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_message_id (message_id, topic, consumer_group),
INDEX idx_business_id (business_id),
INDEX idx_status_retry (status, retry_count),
INDEX idx_parent_msg (parent_message_id)
) ENGINE=InnoDB COMMENT='消息状态追踪表';
2. 订单提交记录表(order_submit_record)
CREATE TABLE order_submit_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL COMMENT '订单号',
request_id VARCHAR(128) NOT NULL COMMENT '请求ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
original_amount DECIMAL(10,2) NOT NULL COMMENT '原始金额',
expected_amount DECIMAL(10,2) NOT NULL COMMENT '预期金额(计算后)',
amount_change DECIMAL(10,2) NOT NULL COMMENT '金额变动',
change_reason VARCHAR(255) COMMENT '变动原因',
business_snapshot JSON COMMENT '业务快照(完整业务数据)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-已提交,1-处理中,2-成功,3-失败,4-已回滚',
rollback_info JSON COMMENT '回滚信息',
mq_message_id VARCHAR(128) COMMENT '关联的消息ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_request_id (request_id),
UNIQUE KEY uk_order_no (order_no),
INDEX idx_user_status (user_id, status),
INDEX idx_mq_message (mq_message_id)
) ENGINE=InnoDB COMMENT='订单提交记录表';
3. 子任务关联表(subtask_relation)
CREATE TABLE subtask_relation (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_business_id VARCHAR(64) NOT NULL COMMENT '父业务ID',
parent_message_id VARCHAR(128) NOT NULL COMMENT '父消息ID',
child_business_id VARCHAR(64) NOT NULL COMMENT '子业务ID',
child_message_id VARCHAR(128) NOT NULL COMMENT '子消息ID',
subtask_type VARCHAR(64) NOT NULL COMMENT '子任务类型:库存/物流/支付',
subtask_status TINYINT NOT NULL DEFAULT 0 COMMENT '子任务状态',
dependency_order INT COMMENT '依赖顺序',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_child_message (child_message_id),
INDEX idx_parent_relation (parent_business_id, parent_message_id),
INDEX idx_subtask_type (subtask_type, subtask_status)
) ENGINE=InnoDB COMMENT='子任务关联表';
完整流程实现
1. 生产者端:可靠消息发送
@Service
@Slf4j
public class OrderSubmitService {
@Autowired
private OrderSubmitRecordMapper orderSubmitRecordMapper;
@Autowired
private MessageStatusMapper messageStatusMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional(rollbackFor = Exception.class)
public SubmitResponse submitOrder(OrderSubmitRequest request) {
// 1. 生成唯一标识
String orderNo = generateOrderNo();
String requestId = request.getRequestId();
String messageId = generateMessageId();
// 2. 校验是否已提交(幂等)
OrderSubmitRecord existRecord = orderSubmitRecordMapper.selectByRequestId(requestId);
if (existRecord != null) {
return buildResponseByRecord(existRecord);
}
// 3. 计算业务数据(如金额变动)
BigDecimal originalAmount = request.getAmount();
BigDecimal expectedAmount = calculateExpectedAmount(originalAmount, request.getCouponId());
BigDecimal amountChange = expectedAmount.subtract(originalAmount);
// 4. 保存提交记录(业务快照)
OrderSubmitRecord record = new OrderSubmitRecord();
record.setOrderNo(orderNo);
record.setRequestId(requestId);
record.setUserId(request.getUserId());
record.setOriginalAmount(originalAmount);
record.setExpectedAmount(expectedAmount);
record.setAmountChange(amountChange);
record.setChangeReason("使用优惠券");
record.setBusinessSnapshot(buildBusinessSnapshot(request));
record.setMqMessageId(messageId);
orderSubmitRecordMapper.insert(record);
// 5. 保存消息状态
MessageStatus messageStatus = new MessageStatus();
messageStatus.setMessageId(messageId);
messageStatus.setBusinessId(orderNo);
messageStatus.setTopic("ORDER_CREATE_TOPIC");
messageStatus.setConsumerGroup("ORDER_CREATE_CONSUMER");
messageStatus.setMessageBody(JSON.toJSONString(buildOrderMessage(request, orderNo)));
messageStatusMapper.insert(messageStatus);
// 6. 发送事务消息
try {
Message<OrderMessage> message = MessageBuilder
.withPayload(buildOrderMessage(request, orderNo))
.setHeader("KEYS", orderNo)
.setHeader("MESSAGE_ID", messageId)
.build();
SendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"ORDER_CREATE_TOPIC:CREATE_TAG",
message,
record // 事务参数
);
if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
throw new RuntimeException("消息发送失败");
}
// 7. 返回处理中状态
return SubmitResponse.processing(orderNo, messageId);
} catch (Exception e) {
log.error("订单提交失败", e);
// 事务会自动回滚,记录会被删除
throw new BusinessException("订单提交失败,请重试");
}
}
// 事务消息的本地事务执行器
@Transactional(rollbackFor = Exception.class)
public void executeLocalTransaction(Message msg, Object arg) {
// 更新消息状态为"已发送"
String messageId = (String) msg.getHeaders().get("MESSAGE_ID");
messageStatusMapper.updateStatus(messageId, MessageStatus.STATUS_SENT);
// 更新订单提交记录状态为"处理中"
OrderSubmitRecord record = (OrderSubmitRecord) arg;
orderSubmitRecordMapper.updateStatus(record.getOrderNo(), OrderStatus.PROCESSING);
}
// 事务消息的回查接口
public LocalTransactionState checkLocalTransaction(Message msg) {
String messageId = (String) msg.getHeaders().get("MESSAGE_ID");
MessageStatus status = messageStatusMapper.selectByMessageId(messageId);
if (status == null) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
if (status.getStatus() == MessageStatus.STATUS_SENT) {
return LocalTransactionState.COMMIT_MESSAGE;
} else {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
}
2. 消费者端:可靠消息处理与子任务分发
@Service
@Slf4j
@RocketMQMessageListener(
topic = "ORDER_CREATE_TOPIC",
selectorExpression = "CREATE_TAG",
consumerGroup = "ORDER_CREATE_CONSUMER",
consumeMode = ConsumeMode.ORDERLY // 顺序消费,保证同一订单的顺序
)
public class OrderCreateConsumer implements RocketMQListener<MessageExt> {
@Autowired
private MessageStatusMapper messageStatusMapper;
@Autowired
private OrderSubmitRecordMapper orderSubmitRecordMapper;
@Autowired
private SubtaskRelationMapper subtaskRelationMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(MessageExt message) {
String messageId = message.getMsgId();
String orderNo = message.getKeys();
// 1. 幂等检查
MessageStatus existStatus = messageStatusMapper.selectByMessageId(messageId);
if (existStatus != null &&
(existStatus.getStatus() == MessageStatus.STATUS_SUCCESS ||
existStatus.getStatus() == MessageStatus.STATUS_PROCESSING)) {
log.info("消息已处理,幂等返回: {}", messageId);
return;
}
// 2. 更新为处理中状态
messageStatusMapper.updateStatus(messageId, MessageStatus.STATUS_PROCESSING);
try {
// 3. 解析消息
OrderMessage orderMessage = JSON.parseObject(message.getBody(), OrderMessage.class);
// 4. 查询业务快照
OrderSubmitRecord record = orderSubmitRecordMapper.selectByOrderNo(orderNo);
if (record == null) {
throw new BusinessException("订单记录不存在: " + orderNo);
}
// 5. 创建订单主记录
Order order = createOrder(record);
// 6. 生成并发送子任务
List<Subtask> subtasks = generateSubtasks(order);
for (Subtask subtask : subtasks) {
sendSubtaskMessage(order, subtask, messageId);
}
// 7. 更新状态
orderSubmitRecordMapper.updateStatus(orderNo, OrderStatus.SUCCESS);
messageStatusMapper.updateStatus(messageId, MessageStatus.STATUS_SUCCESS);
log.info("订单创建成功: {}", orderNo);
} catch (Exception e) {
log.error("订单处理失败", e);
// 8. 处理失败,更新状态并记录重试次数
messageStatusMapper.incrementRetryCount(messageId);
// 检查是否超过最大重试次数
MessageStatus currentStatus = messageStatusMapper.selectByMessageId(messageId);
if (currentStatus.getRetryCount() >= currentStatus.getMaxRetry()) {
messageStatusMapper.updateStatus(messageId, MessageStatus.STATUS_FAILED);
orderSubmitRecordMapper.updateStatus(orderNo, OrderStatus.FAILED);
// 发送到死信队列,人工处理
sendToDeadLetterQueue(message, e);
} else {
// 更新为失败,等待重试
messageStatusMapper.updateStatus(messageId, MessageStatus.STATUS_FAILED);
// 抛出异常,让RocketMQ重试
throw e;
}
}
}
/**
* 发送子任务消息
*/
private void sendSubtaskMessage(Order order, Subtask subtask, String parentMessageId) {
String subtaskMessageId = generateMessageId();
// 保存子任务关联关系
SubtaskRelation relation = new SubtaskRelation();
relation.setParentBusinessId(order.getOrderNo());
relation.setParentMessageId(parentMessageId);
relation.setChildBusinessId(subtask.getBusinessId());
relation.setChildMessageId(subtaskMessageId);
relation.setSubtaskType(subtask.getType());
subtaskRelationMapper.insert(relation);
// 保存子任务消息状态
MessageStatus subtaskStatus = new MessageStatus();
subtaskStatus.setMessageId(subtaskMessageId);
subtaskStatus.setBusinessId(subtask.getBusinessId());
subtaskStatus.setParentMessageId(parentMessageId);
subtaskStatus.setTopic(subtask.getTopic());
subtaskStatus.setConsumerGroup(subtask.getConsumerGroup());
subtaskStatus.setMessageBody(JSON.toJSONString(subtask));
messageStatusMapper.insert(subtaskStatus);
// 发送消息
Message<Subtask> message = MessageBuilder
.withPayload(subtask)
.setHeader("KEYS", subtask.getBusinessId())
.setHeader("MESSAGE_ID", subtaskMessageId)
.setHeader("PARENT_MESSAGE_ID", parentMessageId)
.build();
rocketMQTemplate.syncSend(subtask.getTopic() + ":" + subtask.getTag(), message);
}
}
3. 补偿服务:处理异常和回滚
@Component
@Slf4j
public class CompensationService {
@Autowired
private MessageStatusMapper messageStatusMapper;
@Autowired
private OrderSubmitRecordMapper orderSubmitRecordMapper;
@Autowired
private SubtaskRelationMapper subtaskRelationMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 定时扫描处理异常消息
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行一次
public void scanAndCompensate() {
// 1. 查找处理中的超时消息(处理超过5分钟)
List<MessageStatus> timeoutMessages = messageStatusMapper.selectTimeoutMessages(5);
for (MessageStatus message : timeoutMessages) {
try {
// 2. 检查关联的子任务状态
List<SubtaskRelation> subtasks = subtaskRelationMapper
.selectByParentMessageId(message.getMessageId());
boolean allSubtasksSuccess = subtasks.stream()
.allMatch(subtask -> isSubtaskSuccess(subtask.getChildMessageId()));
if (allSubtasksSuccess) {
// 所有子任务成功,标记主消息成功
messageStatusMapper.updateStatus(message.getMessageId(), MessageStatus.STATUS_SUCCESS);
orderSubmitRecordMapper.updateStatus(message.getBusinessId(), OrderStatus.SUCCESS);
} else {
// 有子任务失败,触发回滚
triggerRollback(message, subtasks);
}
} catch (Exception e) {
log.error("补偿处理失败: {}", message.getMessageId(), e);
}
}
}
/**
* 触发回滚流程
*/
private void triggerRollback(MessageStatus message, List<SubtaskRelation> subtasks) {
log.info("开始回滚流程, messageId: {}", message.getMessageId());
// 1. 查询原始业务记录
OrderSubmitRecord record = orderSubmitRecordMapper
.selectByOrderNo(message.getBusinessId());
if (record == null || record.getStatus() == OrderStatus.ROLLBACKED) {
return;
}
// 2. 发送回滚消息
RollbackMessage rollbackMessage = new RollbackMessage();
rollbackMessage.setOrderNo(message.getBusinessId());
rollbackMessage.setOriginalAmount(record.getOriginalAmount());
rollbackMessage.setExpectedAmount(record.getExpectedAmount());
rollbackMessage.setRollbackReason("子任务处理失败");
rollbackMessage.setBusinessSnapshot(record.getBusinessSnapshot());
// 3. 更新状态为回滚中
orderSubmitRecordMapper.updateStatus(message.getBusinessId(), OrderStatus.ROLLBACKING);
// 4. 发送回滚消息到各个子任务
for (SubtaskRelation subtask : subtasks) {
if (subtask.getSubtaskStatus() == SubtaskStatus.SUCCESS) {
sendRollbackToSubtask(subtask, rollbackMessage);
}
}
// 5. 更新状态
orderSubmitRecordMapper.updateStatus(message.getBusinessId(), OrderStatus.ROLLBACKED);
messageStatusMapper.updateStatus(message.getMessageId(), MessageStatus.STATUS_COMPENSATED);
log.info("回滚流程完成, orderNo: {}", message.getBusinessId());
}
/**
* 发送回滚消息到子任务
*/
private void sendRollbackToSubtask(SubtaskRelation subtask, RollbackMessage rollbackMessage) {
String rollbackTopic = subtask.getSubtaskType() + "_ROLLBACK_TOPIC";
Message<RollbackMessage> message = MessageBuilder
.withPayload(rollbackMessage)
.setHeader("KEYS", subtask.getChildBusinessId())
.setHeader("ORIGINAL_MESSAGE_ID", subtask.getChildMessageId())
.build();
rocketMQTemplate.syncSend(rollbackTopic, message);
}
}
4. 监控告警
@Component
@Slf4j
public class MessageQueueMonitor {
@Autowired
private MessageStatusMapper messageStatusMapper;
/**
* 监控消息积压
*/
@Scheduled(fixedDelay = 300000) // 每5分钟执行一次
public void monitorBacklog() {
// 1. 检查各主题待处理消息数量
List<Map<String, Object>> topicStats = messageStatusMapper
.selectPendingCountByTopic();
for (Map<String, Object> stat : topicStats) {
String topic = (String) stat.get("topic");
Long pendingCount = (Long) stat.get("pending_count");
if (pendingCount > 1000) { // 阈值
// 发送告警
sendAlert("消息积压告警",
String.format("主题%s积压消息%d条", topic, pendingCount));
// 动态扩容消费者
scaleConsumers(topic, calculateScaleCount(pendingCount));
}
}
// 2. 检查失败消息
Long failedCount = messageStatusMapper.selectFailedCount();
if (failedCount > 100) {
sendAlert("失败消息过多",
String.format("当前有%d条失败消息需要处理", failedCount));
}
}
/**
* 生成监控报告
*/
public MonitorReport generateReport() {
MonitorReport report = new MonitorReport();
// 消息处理成功率
report.setSuccessRate(messageStatusMapper.calculateSuccessRate());
// 平均处理时间
report.setAvgProcessTime(messageStatusMapper.calculateAvgProcessTime());
// 各消费者组处理速度
report.setConsumerGroupStats(messageStatusMapper.selectGroupStats());
// 热点业务
report.setHotBusiness(messageStatusMapper.selectHotBusiness(10));
return report;
}
}
总结要点
你的补充点非常关键,完整方案应该包括:
-
消息状态追踪:不仅仅是去重,还要记录处理状态、重试次数
-
消息关联链 :通过
parent_message_id追踪消息树,便于问题排查 -
业务数据快照:保存原始数据,为补偿回滚提供依据
-
子任务管理:一个订单可能拆分为多个异步任务(库存、物流、支付等)
-
事务补偿机制:不仅要能重试,还要能回滚
-
监控告警:实时监控消息积压、处理成功率等指标
这种架构虽然复杂,但提供了极高的可靠性和可追溯性,特别适合金融、电商等对数据一致性要求高的场景。代价是系统复杂度增加,需要维护更多的状态表和监控机制。