干货!详细梳理项目RocketMQ重复消费规避方案

RocketMQ 是一个分布式消息中间件,广泛应用于高并发、高吞吐的场景中。然而,在实际使用中,由于网络抖动、服务重启等原因,可能会导致消息重复消费的问题。以下是我总结日常项目遇到的问题,将详细讲解 RocketMQ 消息重复消费的情况及对应的解决措施。


一、RocketMQ 消息重复消费的原因

1、网络异常

  • 生产者发送消息后,由于网络问题未能及时收到 ACK 确认,生产者会重新发送消息。

2、Broker 故障或重启

  • 如果 Broker 在处理消息时发生故障或重启,可能会导致部分消息未被正确标记为已消费,从而引发重复消费。

3、消费者宕机或重启

  • 消费者在处理完消息后,还未向 Broker 发送消费成功的确认(ACK),就发生了宕机或重启,Broker 会重新投递该消息。

4、幂等性问题

  • 某些业务逻辑本身不具备幂等性,即使消息只被消费一次,也可能因为业务逻辑的设计缺陷导致重复处理。

5、事务消息回查机制

  • 在事务消息场景下,如果生产者提交消息的状态超时,Broker 可能会多次触发回查机制,导致消息被重复消费。

二、解决 RocketMQ 消息重复消费的措施

1. 确保业务逻辑的幂等性

  • 幂等性是指同一操作被执行多次与执行一次的效果相同。这是解决消息重复消费的核心思想。
解决方式:
  • 使用唯一标识符(如消息 ID 或业务主键)记录消息是否已被处理。
  • 在数据库中维护一个“消息状态表”,记录每条消息的消费状态。
示例代码:
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

@Service
@RocketMQMessageListener(topic = "test_topic", consumerGroup = "test_group")
public class MessageConsumer implements RocketMQListener<String> {

    // 假设有一个数据库表 message_status(id, msgId, consumed)
    private final MessageStatusRepository messageStatusRepository;

    public MessageConsumer(MessageStatusRepository messageStatusRepository) {
        this.messageStatusRepository = messageStatusRepository;
    }

    @Override
    public void onMessage(String message) {
        try {
            // 提取消息中的唯一标识(例如:msgId)
            String msgId = extractMsgId(message);

            // 查询消息是否已经被消费
            if (messageStatusRepository.existsByMsgId(msgId)) {
                System.out.println("消息已消费,跳过处理: " + msgId);
                return; // 已消费的消息直接返回
            }

            // 处理业务逻辑
            processBusinessLogic(message);

            // 标记消息为已消费
            messageStatusRepository.save(new MessageStatus(msgId, true));

            System.out.println("消息处理成功: " + msgId);
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("消息处理失败: " + message);
        }
    }

    private String extractMsgId(String message) {
        // 根据业务规则提取消息 ID
        return message.split(":")[0];
    }

    private void processBusinessLogic(String message) {
        // 具体业务逻辑处理
        System.out.println("处理业务逻辑: " + message);
    }
}
数据库表设计:
CREATE TABLE message_status (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    msg_id VARCHAR(255) NOT NULL UNIQUE, -- 消息唯一标识
    consumed BOOLEAN NOT NULL DEFAULT FALSE -- 是否已消费
);

2. 使用全局锁

  • 如果业务场景允许,可以通过分布式锁(如 Redis 或 Zookeeper)确保同一消息不会被多个消费者同时处理。
解决方式:
  • 在消费消息前,尝试获取分布式锁;如果获取失败,则说明该消息正在被其他消费者处理,当前消费者直接跳过。
示例代码:
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RocketMQMessageListener(topic = "test_topic", consumerGroup = "test_group")
public class DistributedLockMessageConsumer implements RocketMQListener<String> {

    private final StringRedisTemplate redisTemplate;

    public DistributedLockMessageConsumer(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void onMessage(String message) {
        try {
            // 提取消息中的唯一标识(例如:msgId)
            String msgId = extractMsgId(message);

            // 尝试获取分布式锁
            String lockKey = "lock:" + msgId;
            Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "LOCKED", 10, TimeUnit.SECONDS);

            if (!lockSuccess) {
                System.out.println("消息正在被其他消费者处理,跳过: " + msgId);
                return; // 已有锁,跳过处理
            }

            // 处理业务逻辑
            processBusinessLogic(message);

            System.out.println("消息处理成功: " + msgId);

        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("消息处理失败: " + message);
        } finally {
            // 释放分布式锁
            String msgId = extractMsgId(message);
            String lockKey = "lock:" + msgId;
            redisTemplate.delete(lockKey);
        }
    }

    private String extractMsgId(String message) {
        // 根据业务规则提取消息 ID
        return message.split(":")[0];
    }

    private void processBusinessLogic(String message) {
        // 具体业务逻辑处理
        System.out.println("处理业务逻辑: " + message);
    }
}

3. 使用 RocketMQ 的顺序消息

  • 如果业务场景要求消息严格按照顺序处理,可以使用 RocketMQ 的顺序消息功能。顺序消息可以避免因乱序导致的重复消费问题。
解决方式:
  • 配置顺序消息队列,确保同一队列的消息按顺序消费。
示例代码:
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class OrderlyMessageConsumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("orderly_topic", "*");

        // 设置顺序消息监听器
        consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                try {
                    String msgId = new String(msg.getBody());
                    System.out.println("顺序消息处理: " + msgId);

                    // 处理业务逻辑
                    processBusinessLogic(msgId);

                    return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUCCESS;
                } catch (Exception e) {
                    e.printStackTrace();
                    return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }
            return org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus.SUCCESS;
        });

        consumer.start();
        System.out.println("顺序消息消费者启动成功...");
    }

    private static void processBusinessLogic(String msgId) {
        // 具体业务逻辑处理
        System.out.println("处理顺序消息业务逻辑: " + msgId);
    }
}

4. 优化消费确认机制

  • 确保消费者在处理完消息后,及时向 Broker 发送消费成功的确认(ACK)。如果消费者在处理过程中发生异常,应捕获异常并重试。
示例代码:
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class ReliableMessageConsumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("reliable_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("test_topic", "*");

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                try {
                    String msgId = new String(msg.getBody());
                    System.out.println("处理消息: " + msgId);

                    // 处理业务逻辑
                    processBusinessLogic(msgId);

                    return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (Exception e) {
                    e.printStackTrace();
                    return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
            return org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("可靠消息消费者启动成功...");
    }

    private static void processBusinessLogic(String msgId) {
        // 具体业务逻辑处理
        System.out.println("处理可靠消息业务逻辑: " + msgId);
    }
}

反正给大家总结起来,就是:

  1. 幂等性:适用于大多数业务场景,是最通用的解决方案。
  2. 分布式锁:适用于对并发处理要求较高的场景。
  3. 顺序消息:适用于严格要求消息顺序的场景。
  4. 优化消费确认机制:适用于需要确保消费可靠性的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瞬间动力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值