切换主题
12. 重试、死信、幂等、补偿:消息会重复,业务要扛得住
线上事故
积分服务出现问题:
同一笔订单给用户加了两次积分。
排查后发现,消费者处理成功后返回结果前超时,消息被再次投递。RocketMQ 这类消息系统通常要按“至少一次”思路设计。也就是说,消费者可能不止一次看到同一条业务事件。
先记住一句话
只要用消息,就要做幂等。
幂等不是高级优化,而是消费端的入门门槛。
幂等键怎么设计
推荐幂等键:
text
eventId如果历史消息没有 eventId,可以退一步:
text
orderId + eventType + consumerName积分服务的处理记录表可以这样设计:
sql
create table message_consume_record (
id bigint primary key,
idempotent_key varchar(128) not null,
consumer_name varchar(64) not null,
status varchar(32) not null,
created_at timestamp not null,
unique key uk_consume_key (idempotent_key, consumer_name)
);消费流程
text
收到消息
-> 生成幂等键
-> 插入消费记录
-> 如果唯一键冲突,说明处理过,直接 SUCCESS
-> 执行业务逻辑
-> 标记处理成功
-> 返回 SUCCESS代码结构:
java
public ConsumeResult consume(MessageView messageView) {
String key = idempotentKeyOf(messageView);
if (!consumeRecordService.tryStart(key, "points-service")) {
return ConsumeResult.SUCCESS;
}
try {
pointsService.addPoints(parseOrderId(messageView));
consumeRecordService.markSuccess(key, "points-service");
return ConsumeResult.SUCCESS;
} catch (Exception ex) {
consumeRecordService.markFailed(key, "points-service", ex.getMessage());
return ConsumeResult.FAILURE;
}
}重试和补偿怎么分工
| 机制 | 解决什么 |
|---|---|
| 消费重试 | 临时失败,例如接口超时、数据库短暂不可用 |
| 死信 | 多次失败后从主消费链路移走 |
| 补偿任务 | 人工或后台重新处理失败业务 |
| 幂等 | 防止重复投递造成副作用 |
不要把所有失败都交给重试。参数错误、数据不存在、业务规则不满足,重试一百次也没用。
小技巧
失败要分类:
| 失败类型 | 处理 |
|---|---|
| 下游超时 | 返回失败,等待重试 |
| 数据暂时未同步 | 短暂重试或延迟补偿 |
| 参数格式错误 | 记录失败,进入人工处理 |
| 业务已经处理过 | 幂等成功,返回 SUCCESS |
常见坑
看到重复消费就怪 MQ。
消息系统不承诺你的业务天然幂等。失败时直接返回 SUCCESS。
这样消息不会进入重试,问题被藏起来。没有失败记录。
没有失败记录,就没有补偿入口。
练习题
- 积分服务的幂等键怎么设计?
- 短信接口超时应该重试还是直接死信?
- 参数解析失败应该无限重试吗?
参考答案
- 优先用
eventId + consumerName,没有 eventId 时用orderId + eventType + consumerName。 - 可以重试,因为可能是临时失败。
- 不应该。参数错误通常重试无效,应记录失败并人工处理。