使用RocketMQ如何处理重复消息

当我们在使用RocketMQ发送消息时,为了实现百分百消息可靠投递,那么重复消息就不可避免。

发送消息(同步方式)一般经过三个步骤:
首先:将消息从生产端发送到broker,生产端继续等待这条消息的处理结果(broker是否能够正常接收);

然后:broker做一些处理(比如同步到从节点以及持久化等);

最后:将应答返还给客户端,客户端收到该消息的应答后,做下一步处理。

我们考虑一下这样的情况:

a. 客户端将一条消息发送(同步方式)到broker,broker成功接收,当broker将成功接收的结果返回给客户端时,这个确认消息在网络传输的过程中,由于某些原因丢失了;

b. 那么对于客户端来说,为了实现高可用机制,在超时后默认有2次重试机会,将这条消息会再次发送给broker;

c. 当broker处理完这条消息后,将成功接收的结果返回给客户端,这次客户端成功的接收到了来自broker发来的确认消息,然后进行下一步的处理;

此时,broker就有两条重复的消息。

RocketMQ为了简化功能上的设计,允许重复消息的存在,将消息去重的任务交给我们自己去处理。在我公司,我的做法如下:

a. 在消息处理之前,先把这条消息的订单号放入redis中(有过期时间),即代表这条消息正在处理中,不允许在同一时间处理两条相同的消息;

b. 根据订单号,在消息消费记录表中查询是否存在(兜底方案,当redis失效时);

    b1. 若存在且处理状态为已成功处理,则返回处理成功并把redis中订单号删除;

    b2. 若存在但处理状态为处理失败,则消费这条消息;

          b2.1 若这条消息消费成功:

                  b2.1.1 在消息消费记录表中的状态置为已成功处理;

                  b2.1.2 将redis中的订单号删除;

                   b2.1.3 返回处理成功;

          b2.2 若这条消息处理失败:

                  b2.2.1 在消息消费记录表中的状态置为处理失败;

                  b2.2.2 将redis中的订单号删除;

                  b2.2.3 返回稍后重试;

    b3. 若不存在,则:

          b3.1 将这条消息插入消费记录表中,处理状态置为处理中;

          b3.2 消费这条消息,若消费成功,则走 b2.1,若消费失败,则走 b2.2

伪代码如下:

public class PkgConcurrentlyListener implements MessageListenerConcurrently {
    protected static Logger logger = LoggerFactory.getLogger(PkgConcurrentlyListener.class);
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        MessageExt me = list.get(0);
        String topic = me.getTopic();
        String tags = me.getTags();
        String keys = me.getKeys();
        boolean result = getLock30Second(keys,keys);
        if(!result){
            //正在处理中,请稍后再试
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        try {
            String msgBody = new String(me.getBody(), RemotingHelper.DEFAULT_CHARSET);
            logger.info("消费者消费消息 topic = 【" + topic + "】keys = 【" + keys + "】,msgBody = 【" + msgBody + "】");
            
            //根据keys在消费记录表中查询数据
            
            
            //业务的处理逻辑

            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            e.printStackTrace();
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;//消息处理失败,过会再试
        } finally {
            //无论消费成功与否,都删除
            RedisPoolUtil.del(keys);
        }
    }
    /**
     * 锁住某个key值30S,需要解锁时删除即可
     * 注意:不要使用这种方式,因为这种做法不能防死锁,使用这篇文章    
     * https://blog.csdn.net/zhaoming19870124/article/details/91041855介绍的优化版本
     */
    public static boolean getLock30Second(final String key, final String value) {
        Jedis jedis = RedisPoolConfig.getJedis();
        try {
            Long flag = jedis.setnx(key, value);
            System.out.println("将key = 【" + key + "】,放入redis中的结果 = 【" + flag + "】");
            if (1 == flag) {
                //如果在redis中不存在,则设置有效期,防止永久存在;
                //当这个keys永久存在时,若一条消息在第一次消费时,消费失败了,则永远不会被消费了。
                // 因为接下来的每次消费,都被redis拦下来了
                flag = jedis.expire(key, 30);
            }
            return flag == 1L ? true : false;
        } catch (Exception e) {
            System.out.println(e);
            return false;
        }
    }
}

 

 

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值