异步重试、消息队列、Saga模式三连击,让你的请求“死了都要爱”!
开篇:当熔断器变成“数据黑洞”
“服务终于恢复了,但用户投诉‘我的订单怎么消失了?’”
“促销活动明明显示成功,后台却查不到记录……”
——熔断机制保护了系统,却让用户请求“人间蒸发”。今天教你用补偿机制造一套“复活甲”,让丢失的请求“起死回生”!
一、熔断后的“烂摊子”:请求去哪了?
1. 熔断器的“冷酷逻辑”
- 熔断开启时:所有请求直接拒绝(快速失败),不会真正调用下游服务
- 残酷现实:用户看到“系统繁忙”,但服务恢复后请求不会自动重试
2. 丢失请求的三大灾难现场
- 电商下单:扣款成功但订单未生成,用户钱货两空
- 支付回调:第三方支付成功,本地服务未记录
- 库存扣减:超卖或库存锁定未释放
二、补偿机制设计:四大“复活术”
方案1:客户端重试——简单但危险
- 原理:前端提示“正在重试”,自动重新发起请求
- 代码示例(Axios):
async function placeOrder() {
let retries = 3;
while(retries > 0) {
try {
await axios.post('/api/order', data);
break;
} catch (error) {
retries--;
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
- 适用场景:短时故障(如网络抖动)
- 致命缺陷:
- 用户可能离开页面导致中断
- 重复提交风险(如重复扣款)
方案2:服务端异步重试——可靠但复杂
- 核心步骤:
- 请求入口层记录原始请求到
重试表
- 定时任务扫描失败请求,逐步重试(指数退避)
- 成功后将状态标记为已完成
- 请求入口层记录原始请求到
- 代码实现(Spring Boot + JPA):
@Transactional
public void saveRetryTask(OrderRequest request) {
RetryTask task = new RetryTask();
task.setStatus("PENDING");
task.setRequestBody(serialize(request));
retryTaskRepository.save(task);
}
@Scheduled(fixedDelay = 5000)
public void retryFailedTasks() {
List<RetryTask> tasks = retryTaskRepository.findByStatusAndRetryCountLessThan("PENDING", 5);
tasks.forEach(task -> {
try {
OrderRequest request = deserialize(task.getRequestBody());
orderService.process(request);
task.setStatus("SUCCESS");
} catch (Exception e) {
task.setRetryCount(task.getRetryCount() + 1);
}
retryTaskRepository.save(task);
});
}
- 避坑指南:
- 数据库需加索引防止全表扫描
- 重试间隔逐步增加(如1s → 5s → 30s)
方案3:消息队列持久化——高可用首选
- 操作流程:
- 请求入口将消息发送到MQ(如RocketMQ/Kafka)
- 消费者处理消息,失败时MQ自动重投
- 超过重试次数进入死信队列人工处理
- 代码示例(RocketMQ):
// 生产者
public void sendOrderMessage(Order order) {
Message message = new Message("ORDER_TOPIC", "TAG_A", order.toString().getBytes());
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 发送成功
}
@Override
public void onException(Throwable e) {
// 记录到本地重试表
saveRetryTask(order);
}
});
}
// 消费者
@RocketMQMessageListener(topic = "ORDER_TOPIC", consumerGroup = "order-group")
public class OrderConsumer implements RocketMQListener<Order> {
@Override
public void onMessage(Order order) {
try {
orderService.process(order);
} catch (Exception e) {
throw new RuntimeException("触发MQ重试");
}
}
}
- 优势:
- 消息持久化,断电不丢失
- 天然支持重试和流量削峰
- 注意事项:
- 消费逻辑需幂等(防重复消费)
- 死信队列需监控告警
方案4:Saga模式——分布式事务终极方案
- 适用场景:跨多个服务的补偿(如订单→库存→支付)
- 两种实现:
-
编排式(Orchestration):
- 中央协调器(如状态机)控制流程
- 每个服务提供正向操作和补偿接口
-
事件驱动式(Choreography):
- 服务间通过事件发布/订阅通信
- 每个服务监听相关事件并触发补偿
-
- 代码示例(使用Camunda工作流引擎):
// 定义Saga流程
@Bean
public ProcessEngineConfiguration processEngine() {
return new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:mysql://localhost:3306/camunda")
.setDatabaseSchemaUpdate("true");
}
// 订单服务补偿
@Service
public class OrderCompensation {
@Autowired
private RuntimeService runtimeService;
public void compensateOrder(String orderId) {
runtimeService.createProcessInstanceByKey("OrderSaga")
.setVariable("orderId", orderId)
.execute();
}
}
三、避坑指南:补偿机制的“七伤拳”
-
无限重试陷阱:
- 现象:异常未修复导致重试风暴,拖垮系统
- 解法:设置最大重试次数 + 人工介入阈值
-
幂等性缺失:
- 现象:重试导致重复下单/扣款
- 解法:
- 业务层幂等校验(如订单号唯一索引)
- 数据库乐观锁(version字段)
-
消息顺序混乱:
- 现象:先执行了“取消订单”再处理“创建订单”
- 解法:
- RocketMQ支持顺序消息
- Kafka按分区Key保序
-
补偿事务失败:
- 现象:补偿操作本身出错(如回滚库存时服务宕机)
- 解法:
- 记录补偿日志,定时扫描恢复
- 设计最终一致性而非强一致性
四、终极选择:没有完美方案,只有权衡取舍
- 简单系统:服务端异步重试 + 数据库幂等
- 高并发场景:消息队列 + 死信队列监控
- 跨服务事务:Saga模式 + 事件溯源
- 关键业务:多级补偿(客户端提示 + 服务端重试 + 人工对账)
记住:
“好的补偿机制不是消灭问题,而是让问题可追踪、可修复!”