RocketMq消息幂等性

283 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

背景

由于RocketMQ会出现重复消息,重复消息消费对于业务会造成影响,所以业务系统都需要考虑消息的幂等性。

简介

消息幂等性:消息重复消费多次与消息消费一次的结果都相同,并且多次消费对不会业务系统造成任何影响。

RocketMq产生重复消息场景

生产者发送消息时重复

当一条消息已被成功发送到Broker并完成持久化,此时出现了网络波动,从而导致Broker对生产者应答失败。此时生产者认为消息发送失败并尝试再次发送消息,此时Broker中就可能会出现两条内容相同的消息。

消费者消费失败

消费者消费失败,会重新把消费发回给Broker进行处理,Broker收到消息后会重新发送给消费者,所以会产生重复消息,该问题上一篇文章中已经提到了。

Rebalance时消息重复

当Consumer Group中的Consumer数量发生变化时,或其订阅的Topic的Queue数量发生变化时,会触发Rebalance,此时Consumer可能会收到曾经被消费过的消息。

解决方案

生成发送消息时重复解决方案

针对发送消息重复的场景,可以通过数据库唯一索引来进行业务保证。

图片.png

针对上述的表结构需要将bizkey+type设置联合的唯一索引,确保每种业务类型的消息唯一性。

消费者重复消息解决方案

针对消费者重复消息的解决方案,可以采用如下的几种方案。

1.采用数据库乐观锁实现. 2.采用Redis的写入,因为Redis的写入操作支持天然的幂等性。

具体实现

生产者解决实现

  public void sendOrderMessage(String topic,String bizKey,String type,String content)
    {
        logger.info("send add order message ");
            Message message =new Message();
            //业务的主键:可以为订单id、商品id等
            message.setKeys(bizKey);
            message.setTopic(topic);
            String messageId=UUID.randomUUID().toString();
            message.putUserProperty("messageId", messageId);
            //创建订单
            message.putUserProperty("type", type);
            try
            {
                message.setBody(content.getBytes(RemotingHelper.DEFAULT_CHARSET));
            }
            catch (Exception e)
            {
                logger.error("message body getBytes error",e);
            }
            
            SysMessageInfo sysMessageInfo =new SysMessageInfo();
 
            // 发送消息
            rocketMQTemplate.asyncSend(topic, message,new SendCallback()
            {
                @Override
                public void onSuccess(SendResult result)
                {
                    logger.info("发送消息成功");
                    //业务日志记录成功
                    messageService.addProductMessge(sysMessageInfo);
                }
                
                @Override
                public void onException(Throwable e)
                {
                    logger.error("发送消息失败",e);
                    //业务进行日志记录失败
                    messageService.addProductMessge(sysMessageInfo);
                }
            });
    } 

消息插入接口

 public void addProductMessge(SysMessageInfo sysMessageInfo)
    {
        try
        {
            // 查询数据是否存在,如果存在执行更新操作,否则执行新增操作
            SysMessageInfo messageInfo = getMessage(sysMessageInfo);
            if(messageInfo!=null)
            {
               //更新消息的时间和版本号
               updateMessage(sysMessageInfo);    
            }
            //执行新增操作
            else
            {
                addMessage(sysMessageInfo);
            }
        }
        catch (Exception e)
        {
            logger.error("addProductMessge error",e);
        }
    }

说明:通过数据库的唯一索引来保证消息重复,先查询消息是否存在,如果不存在则执行新增操作,否则执行更新操作。

消费者实现

    private Logger logger =LoggerFactory.getLogger(getClass());
    
    @Autowired
    private MessageService messageService;
    
    @Override
    public void onMessage(MessageExt messageExt)
    {
        String msg=JSON.toJSONString(messageExt);
        logger.info("send messsge succss content is:{}", msg);
        //消息id
        String messageId= messageExt.getProperty("messageId");
        //业务key
        String bizKey=messageExt.getKeys();
        //类型
        String type= messageExt.getProperty("type");
        
        SysMessageInfo sysMessageInfo =new SysMessageInfo();
        sysMessageInfo.setBizKey(bizKey);
        sysMessageInfo.setType(type);
        sysMessageInfo.setMessageId(messageId);
        
        // 查看消息是否消费,true表示消息已经消费
        if(messageService.checkReaptMesasge(sysMessageInfo))
        {
            logger.error("message already counsumer:messageId:{}",messageId);
            return;
        }
        
        /**
         * 业务处理逻辑,
         * 可以通过业务状态的变化来判断消息是否消费
         * 通过乐观锁来保证业务并发性
         * 如果业务场景复杂可以采用分布式来实现
         */
        invokeBiz(sysMessageInfo);
    }
    
    /**
     * 处理
     * @param sysMessageInfo
     */
    public void invokeBiz(SysMessageInfo sysMessageInfo)
    {
         //更新消息状态
         //业务接口
         
    }

消息消费检查接口

@Override
    public boolean checkReaptMesasge(SysMessageInfo sysMessageInfo)
    {
        //存在并发问题
        SysMessageInfo dbMessage =getMessage(sysMessageInfo);
        
        // 消息已消费
        if(dbMessage!=null && "Consumed".equals(dbMessage.getStatus()))
        {
            return true;
        } 
        return false;
    }

说明:消息消费检查接口可能会存在并发问题,所以还是需要业务通过乐观锁或者流程状态来进一步判断重复消费。

总结

RocketMQ无法做到消息的幂等性,RocketMQ能够尽量保证消息不丢失,且遵循至少被消费一次的原则,具体的消息幂等还是需要结合实际的业务场景,通过相应的技术方案来保证。