消息队列:MQ消息幂等 - 从重复根源到防重实战

MQ消息可靠从架构设计上,主要有三个核心设计点:1、超时重传;2、消息落地;3、高可用设计;而超时重传与高可用故障恢复时,都可能带来重复消息的问题。

那么,怎么应对重复消息,实现幂等呢?今天我们就要讨论下这个话题

01.重复消息的产生

什么情况下会导致消息重复呢?可以从生产者、消息代理层和消费者三个层面进行分析。

生产者端

生产者发送了一条消息,但没收到Broker的确认(可能是网络超时),重试发送。但实际上,第一次发送可能已经成功,Broker只是没来得及回复,导致了同一条消息发送了两次。

生产者: "喂,收到了吗?"

Broker: [沉默](其实已收到,但回复丢了)

生产者: "没回应?那我再发一次吧!"

这就像你发微信,对方没回,你以为没收到又发了一遍。结果对方回复:"别发了,我看到了!"

消息代理层

采用主从架构的消息队列。当主节点宕机,从节点接管时,可能会出现这样的情况:部分已经被消费的消息,因为确认信息还未同步到从节点,导致消息被重新投递。

消费者: "我已经处理完消息A了"(对主节点说)

主节点: [突然宕机,确认信息未同步到从节点]

从节点: [接管服务] "这条消息好像没人处理过,我得重新投递"

消费者: "咦?这消息我明明处理过了啊!"

这就像团队协作时,组长突然请假,接手的人不知道哪些工作已经分配出去了,于是重新分配了一遍。

消费者端

消费者端采用手动确认机制。但如果消费者在处理完消息后,发送确认信息前崩溃了,那么这条消息会被重新投递给其他消费者。

消费者: "我已经处理完了,正准备告诉Broker..."

系统崩溃

Broker: "没收到确认,这消息得重新发给别人处理"

这就像你完成了工作,正准备汇报时电脑蓝屏了,领导以为你没做,又把任务分给了别人。

02.如何应对重复消息?

在生产者、消息代理、消费者都可能产生重复消息,那我们就可以从三个层面进行防重处理。

1. 生产者端防重

为消息生成全局ID,即便重复发送,也能识别并去重。全局ID满足:

全局唯一;

由MQ生成,具备业务无关性,对生产者与消费者都透明;

2. 消息代理层防重

消息代理层的防重,可以从消息ID跟踪、时间窗口去重、消息投递状态持久化等方式实现。

  • 利用唯一ID + 存储:在消息代理层维护已处理消息ID的存储;
  • 时间窗口机制:只在一定时间窗口内进行消息去重;
  • 基于内容的去重:计算消息内容哈希作为去重依据;
  • 顺序号机制:使用严格递增的序列号检测重复消息;
  • 消息投递状态持久化:记录消息处理的全过程状态,支持查询与追踪;
  • 分布式协调:使用分布式锁或一致性协议确保消息唯一性;
  • 消息代理层节点异常是不可避免的,完善的设计需要同时具备可靠的节点恢复机制和合理的消息投递策略,同时与消费端幂等处理形成完整的防重体系。

3. 消费者防重

消费者端是防止重复消息影响的最后一道防线,也是最关键的一环。无论生产者和代理层如何努力,由于分布式系统的固有特性,消息重复问题难以完全避免。我们需要确保消息处理的幂等性,即同一条消息被处理多次和一次的效果是一样的。

幂等设计的核心在于业务层面的设计- 将非幂等操作重构为幂等操作,通过精心设计的数据结构和处理流程,使系统在面对重复输入时保持稳定和一致。

实现幂等性的常用方法:

1. 数据库唯一约束:利用数据库的唯一索引特性,防止重复插入。

java 复制代码
CREATE TABLE orders (
    order_id VARCHAR(32) PRIMARY KEY,  -- 使用订单ID作为主键
    user_id VARCHAR(32) NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status VARCHAR(10) NOT NULL
);

2. 状态检查:处理前先检查状态,避免重复操作。

java 复制代码
public void processPaymentMessage(PaymentMessage message) {
    String orderId = message.getOrderId();
    
    // 先检查订单状态
    Order order = orderRepository.findById(orderId);
    if (order != null &&  (OrderStatus.PAID == order.getStatus())) {
        log.info("订单{}已支付,忽略重复消息", orderId);
        return;
    }
    
    // 处理支付逻辑
    orderService.updateOrderStatus(orderId, "PAID");
}

3. 分布式锁:使用Redis或Zookeeper实现分布式锁,确保同一时间只有一个消费者在处理特定消息。

java 复制代码
public void processMessage(Message message) {
    String messageId = message.getId();
    String lockKey = "lock:" + messageId;
    
    boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (!locked) {
        log.warn("消息{}正在被其他实例处理,跳过", messageId);
        return;
    }
    
    try {
        // 处理消息
        doProcess(message);
    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}

4. 消息处理记录:维护一个已处理消息的记录表,处理前先查询是否已处理。

java 复制代码
public void processMessage(String messageId, String content) {
      if (messageRepository.exists(messageId)) {
          log.info("消息已处理,跳过: {}", messageId);
          return;
      }
      
      // 业务处理逻辑
      businessService.process(content);
      
      // 记录消息已处理
      messageRepository.save(new MessageRecord(messageId));
  }

5. 乐观锁/版本控制:利用数据版本号控制并发更新。

java 复制代码
UPDATE accounts 
SET balance = balance - 100, version = version + 1 
WHERE account_id = 'A123' AND version = 1

6. 基于Token的幂等机制:操作前申请幂等Token,提交时校验并销毁。

java 复制代码
// 申请支付Token
String token = tokenService.generateToken(userId, orderId);

// 支付时验证Token
if (tokenService.validateAndInvalidate(userId, orderId, token)) {
    paymentService.processPayment(orderId);
} else {
    throw new InvalidTokenException("无效或已使用的Token");
}

实践建议

根据业务特性选择合适的幂等方案,可组合多种方法

设计幂等解决方案时考虑性能、复杂度和易用性平衡

引入消息处理结果记录,便于问题排查和数据修复

在分布式系统中,消费端幂等设计是保障数据一致性的最经济有效手段

03.实战案例:订单系统的幂等性设计

在电商订单系统中,我们采用了多层防护策略来处理重复消息:

生产者端:使用订单号+时间戳作为消息ID,确保消息唯一性。

消息代理层:启用RocketMQ的消息去重功能,设置5分钟的去重时间窗口。

消费者端:

订单创建:利用订单号的唯一索引,防止重复创建

支付确认:先查询订单状态,只有未支付的订单才会更新

库存扣减:使用数据库行锁+版本号机制,确保库存操作的准确性

积分增加:维护操作日志表,记录每次积分变更的来源和消息ID

这套机制上线后,即使在网络不稳定的情况下,我们也能确保订单处理的准确性,不会出现重复扣款、重复发货等问题。

总结

在消息队列系统中,重复消息是不可避免的,它是我们追求消息必达而付出的代价。关键在于我们如何应对这些重复消息,将它们的影响降到最低。

在生产者端尽量避免重复,在消息代理层尽量识别重复,在消费者端一定要能够处理重复。

相关推荐
天微微蓝sunny12 小时前
Pulsar IO 应用场景及案例
消息队列·pulsar
014-code3 天前
Kafka + Spring Boot 实战入门
java·spring boot·kafka·消息队列
Chan169 天前
从生产到消费:Kafka 核心原理与实战指南
java·spring boot·分布式·spring·java-ee·kafka·消息队列
茶杯梦轩16 天前
从零起步学习RabbitMQ || 第二章:RabbitMQ 深入理解概念 Producer、Consumer、Exchange、Queue 与企业实战案例
服务器·后端·消息队列
初次攀爬者17 天前
Kafka 基础介绍
spring boot·kafka·消息队列
初次攀爬者18 天前
RocketMQ 消息可靠性保障与堆积处理
后端·消息队列·rocketmq
初次攀爬者18 天前
RocketMQ 集群介绍
后端·消息队列·rocketmq
初次攀爬者18 天前
RocketMQ 基础学习
后端·消息队列·rocketmq
初次攀爬者19 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
DemonAvenger21 天前
Kafka性能调优:从参数配置到硬件选择的全方位指南
性能优化·kafka·消息队列