参考:2020黑马——消息中间件RabbitMQ以及【学相伴】RabbitMQ最新完整教程IDEA版通俗易懂
目录
1 分布式数据一致性问题
当业务拆分成多个异步微服务在不同的节点运行的时候,需要考虑数据一致性问题,单机上可以使用数据库的事务,遇到问题可以回滚,那么多机怎么办?
解决办法:
- 两阶段提交(2PC):分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):
- 本地消息表:本地消息表和业务数据表处于同一个数据库中,利用本地事务来保证两个表的事务。
- 消息队列法:利用消息队列实现分布式事务。
2 消息队列法
客户端把请求不是直接发送到服务端,而是发到消息队列,利用消息队列来实现分布式事务。
2.1 可靠生产
客户端把消息发送到消息队列,要知道这个消息确实发送到了消息队列,而没有丢失,所以可以在生产者端添加消息确认机制,即发送了消息,消息队列要回复客户端说确实已经到了,即消息确认。同时客户端需要有消息冗余表的存放,如果消息队列出了问题,客户端还是有数据,可以继续投放。
消息冗余表:{消息数据本身,消息状态},消息状态由消息队列确认机制来更新,确保消息确实到了消息队列。
2.1.1 首先配置文件中开启确认机制
# 配置RabbitMQ服务
spring:
rabbitmq:
publisher-confirm-type: correlated # 确认机制
2.1.2 业务服务中编写确认逻辑
// 消息确认机制
@PostConstruct // 用来修饰非静态void方法,在服务器加载servlet的时候运行,并且只执行一次,在构造函数之后,init()方法之前执行
public void CallBack(){
// 消息发送成功以后,就有消息回执
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
// 如果不成功
if(!ack) LOGGER.info("消息投递失败:"+correlationData.getId());
//成功
else LOGGER.info("消息投递成功"+correlationData.getId());
}
});
}
在消息确认之后可以修改相应数据库的状态值。
2.2 可靠消费
消费者消费完毕之后,会把ack消息发给消息队列,说这个消息确实被我消费完毕了,没有发生错误,如果发生错误,由消费者来控制这个消息的重发、清除和丢弃。
RabbitMQ消费者在消费消息的时候,如果出现异常,会不停反复重试,出现死循环。
解决方法:1.控制重发的次数 2.try-catch-手动ack 3.try-catch-手动ack+死信队列
2.2.1 更改配置文件手动应答
# 配置RabbitMQ服务
spring:
rabbitmq:
# 消费者手动ack
listener:
simple:
acknowledge-mode: manual
retry:
enabled: true # 开启重试
max-attempts: 5 # 最大重试次数
initial-interval: 50ms # 重试间隔时间
2.2.2 创建队列绑定死信队列
// 2.声明队列
@Bean
public Queue emailQueue() {
// 设置过期时间
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "lwj_dead_exchange");//添加死信队列
// 名字、是否持久化
return new Queue("email.fanout.queue", true, false, false, args);
}
消息发生错误的时候,把消息发送给死信交换机。
2.2.3 消费者逻辑
// 消费消息方法
@RabbitListener(queues = {"email.fanout.queue"})
public void receiveEmail(String message, Channel channel, CorrelationData correlationData,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
// try消费信息
try {
Order order = JSON.parseObject(message, Order.class);
// 如果是王五就异常
if(order.getUserName().equals("王五")) throw new Exception();
// 不是王五正常消费
LOGGER.info("消费一条邮件服务:" + message);
// 手动确认
channel.basicAck(tag,false);
}catch (Exception e){
// 捕捉到异常
channel.basicNack(tag,false,false);// 不重试
}
}
例子:当订单的消费者是王五的时候,就把消息发送到死信队列,不进行确认。其他情况都进行确认。
2.2.4 为什么可靠消费可以实现分布式事务
分布式事务实际上都是转换成本地事务,消息队列只是该本地事务是否被正常执行的一种通知机制,如果一个任务有三个服务,两个正常执行,一个不正常执行,不正常执行的消息会在本地事务回滚,然后把消息通道到死信队列,服务端的死信队列消费者会发现这个消息,然后启动回滚服务,告诉另外两个程序,也启动回滚事务,达到数据一致性。