RabbitMQ可靠消息最终一致性

可靠消息最终一致性要解决的问题

在这里插入图片描述再整个过程中可能由于网络通信不确定性会导致整个操作失败

1.上游服务把信息发送成功
2.下游服务把消息成功消费
3.对消息做幂等(由于消费者消费完但是由于网络问题导致响应ack失败,会导致队列中一直存在这个已经消费的消费,导致消费者重复消费,而做幂等就是解决这种情况)

解决方案

在这里插入图片描述
以新增商品到mysql,然后将新增的商品同步到ES的索引库中
整体的思路是:首先创建一张表(local_message),用来存放需要发送的消息

DROP TABLE IF EXISTS `local_message`;
CREATE TABLE `local_message` (
  `tx_no` varchar(255) NOT NULL,
  `item_id` bigint DEFAULT NULL,
  `state` int(11) DEFAULT NULL,
  PRIMARY KEY (`tx_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.消息生产者在处理完业务后(比如新增某件商品后到mysql,需要将这件商品的信息添加到ES中,我们需要将该商品的ID作为消息发送到消息队列中,消费者获取该ID可以调用商品服务查询出来然后插入ES索引库中)
2.先把信息存入到local_message这张表中,设置状态为0,表示还没有被成功发送到RabbitMq
3.开启定时任务,扫描这张表中状态为0的数据,进行发送操作,当发送成功成功响应的时候,改变这张表中这条信息的状态为1,这样就可以区别那些发送成功,那些发送失败。

实例:
(1)

  @Override
    public void insertTbItem(TbItem tbItem, String desc, String itemParams) {
        //保存商品信息

        //保存本地消息记录
        LocalMessage localMessage = new LocalMessage();
        localMessage.setTxNo(UUID.randomUUID().toString());
        localMessage.setItemId(itemId);
        localMessage.setState(0);
        localMessageMapper.insertSelective(localMessage);
    }

再本地商品添加后,将商品的信息封装到localmessage插入localmessage表中

接着设置定时任务,将localmessage表中的数据作为消息,发送到RabbitMq(设置定时任务是为了防止由于网络因素导致发送失败,固定间隔时间发送状态为0的localmessage消息确保最终能发送成功)
(2)
进行定时任务配置

package com.bjpowernode.config;

import com.bjpowernode.quartz.ItemQuartz;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfig {

    //job:做什么事
    @Bean
    public MethodInvokingJobDetailFactoryBean 
           			methodInvokingJobDetailFactoryBean(ItemQuartz itemQuartz) {
        MethodInvokingJobDetailFactoryBean JobDetailFactoryBean = 
            							new MethodInvokingJobDetailFactoryBean();
        JobDetailFactoryBean.setTargetObject(itemQuartz);
        JobDetailFactoryBean.setTargetMethod("scanLocalMessage");
        return JobDetailFactoryBean;
    }

    //trigger:什么时候做
    @Bean//trigger(job)
    public CronTriggerFactoryBean cronTriggerFactoryBean(
            				MethodInvokingJobDetailFactoryBean JobDetailFactoryBean) {
        CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
        triggerFactoryBean.setCronExpression("*/1 * * * * ?");
        triggerFactoryBean.setJobDetail(JobDetailFactoryBean.getObject());
        return triggerFactoryBean;
    }

    //scheduled:什么时候做什么事
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(
        								CronTriggerFactoryBean triggerFactoryBean) {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setTriggers(triggerFactoryBean.getObject());
        return schedulerFactoryBean;
    }
}

编写消息发送代码

package com.bjpowernode.mq;

import com.bjpowernode.mapper.LocalMessageMapper;
import com.bjpowernode.pojo.LocalMessage;
import com.bjpowernode.utils.JsonUtils;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ConfirmCallback;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ItemMQSender implements ReturnCallback, ConfirmCallback{

    @Autowired
    private LocalMessageMapper localMessageMapper;

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMsg(LocalMessage localMessage) {
        RabbitTemplate rabbitTemplate = (RabbitTemplate) this.amqpTemplate;
        rabbitTemplate.setConfirmCallback(this);//确认回调
        rabbitTemplate.setReturnCallback(this);//失败回退

        //用于确认之后更改本地消息状态或删除本地消息--本地消息id
        CorrelationData correlationData = new CorrelationData(localMessage.getTxNo());
        rabbitTemplate.convertAndSend("index_exchange","item.add",
                JsonUtils.objectToJson(localMessage),correlationData);
    }

    /**
     * 失败回调
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, 
                                			String exchange, String routingKey) {
        System.out.println("return--message:" + new String(message.getBody()) 
                           + ",exchange:" + exchange + ",routingKey:" + routingKey);
    }
    /**
     * 确认回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            // 消息发送成功,更新本地消息为已成功发送状态或者直接删除该本地消息记录
            String txNo = correlationData.getId();
            LocalMessage localMessage = new LocalMessage();
            localMessage.setTxNo(txNo);
            localMessage.setState(1);
            localMessageMapper.updateByPrimaryKeySelective(localMessage);
        }
    }
}

设置定时任务发送消息

package com.bjpowernode.quartz;

import com.bjpowernode.mq.ItemMQSender;
import com.bjpowernode.pojo.LocalMessage;
import com.bjpowernode.service.LocalMessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

@Component
public class ItemQuartz {

    @Autowired
    private LocalMessageService localMessageService;

    @Autowired
    private ItemMQSender itemMQSender;

    /**
     * 关闭超时订单
     * 检查本地消息表
     */
    public void scanLocalMessage(){
        System.out.println("执行扫描本地消息表的任务:" + new Date());
        List<LocalMessage> localMessageList = 
            		localMessageService.selectlocalMessageByStatus();
        for (int i = 0; i < localMessageList.size(); i++) {
            LocalMessage localMessage = localMessageList.get(i);
            itemMQSender.sendMsg(localMessage);
        }
    }
}

在这里插入图片描述

对消息做幂等

上游服务已经能够确保把消息发送到RabbitMq,此时下游的消息消费者可能由于网络问题,在接受到消息后在返回ACK时网络出现问题失败,导致队列中一直存在该消息,消费者监听队列中的消息默认一直消费
对此,为了防止这种情况的发生我们还需要创建一个去重表,用来做幂等的控制

DROP TABLE IF EXISTS `msg_distinct`;
CREATE TABLE `msg_distinct` (
  `tx_no` varchar(255) NOT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`tx_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

实现的思路是:1.首先消费者接受队列中的localmessage消息,2.接着先查这张去重表(用localmessage中的tx_no作为条件查询),当为空的时候说明第一次消费,3.执行消费者业务(ES的索引库中添加信息),4.然后向去重表中添加数据,做ack响应,如果不为空,说明之前添加过,可能由于网络原因导致ack发送失败没有将该数据从队列中移除,此时需要将该消息移除,不断的发送ack直到网络正常,删除该消息。
监听者

package com.bjpowernode.listener;

import com.bjpowernode.pojo.LocalMessage;
import com.bjpowernode.pojo.MsgDistinct;
import com.bjpowernode.service.MsgDistinctService;
import com.bjpowernode.service.SearchItemService;
import com.bjpowernode.utils.JsonUtils;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class SearchMQListener {

    @Autowired
    private MsgDistinctService msgDistinctService;
    @Autowired
    private SearchItemService searchItemService;

    /**
     * 监听者接收消息三要素:
     *  1、queue
     *  2、exchange
     *  3、routing key
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value="index_queue"),
            exchange = @Exchange(value="index_exchange",type= ExchangeTypes.TOPIC),
            key= {"item.*"}
    ))
    public void listen(String msg, Channel channel, Message message)
            throws IOException {
        System.out.println("接收到消息:" + msg);
        LocalMessage localMessage = JsonUtils.jsonToPojo(msg, LocalMessage.class);

        //进行幂等判断,防止ack应为网络问题没有送达,导致扣减库存业务重复执行
        MsgDistinct msgDistinct =
                msgDistinctService.selectMsgDistinctByTxNo(localMessage.getTxNo());
        if(msgDistinct==null){
            //执行业务,向es库中插入数据
            searchItemService.insertDocument(localMessage.getItemId());
            //向去重表中插数据
            msgDistinctService.insertMsgDistinct(localMessage.getTxNo());
        }else{
            System.out.println("=======幂等生效:事务"+msgDistinct.getTxNo()
                    +" 已成功执行===========");
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值