Skip to content

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

常见坑

  1. 看到重复消费就怪 MQ。
    消息系统不承诺你的业务天然幂等。

  2. 失败时直接返回 SUCCESS。
    这样消息不会进入重试,问题被藏起来。

  3. 没有失败记录。
    没有失败记录,就没有补偿入口。

练习题

  1. 积分服务的幂等键怎么设计?
  2. 短信接口超时应该重试还是直接死信?
  3. 参数解析失败应该无限重试吗?

参考答案

  1. 优先用 eventId + consumerName,没有 eventId 时用 orderId + eventType + consumerName
  2. 可以重试,因为可能是临时失败。
  3. 不应该。参数错误通常重试无效,应记录失败并人工处理。

来源

Built with VitePress. Deployed on Cloudflare Pages.