可靠消息最终一致性要解决的问题
再整个过程中可能由于网络通信不确定性会导致整个操作失败
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);
}
}