一、RabbitMQ实战系列一
1.1 消息的可靠投递
1.2 消息的可靠投递2
1.3 消息的可靠投递3
1.2 消息持久化
1.3 消息确认机制之消息的准确发布
1.4 消息确认机制之消息的正确消费
二、RabbitMQ实战系列二
2.1 RabbitMQ系列(一)--消息中间件MQ如何去选择
2.2 RabbitMQ系列(二)--基础组件
2.3 RabbitMQ系列(三)--Java API
2.4 RabbitMQ系列(四)--消息如何保证可靠性传输以及幂等性
2.5 RabbitMQ系列(五)--高级特性
2.6 RabbitMQ系列(六)--面试官问为什么要使用MQ,应该怎么回答
2.7 RabbitMQ系列(七)--批量消息和延时消息
2.8 RabbitMQ系列(八)--顺序消费模式和迅速消息发送模式
三、RabbitMQ保证消息的可靠性传输
3.1 生产者弄丢了数据
⽣产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为⽹络问题啥的,都有可能。
此时可以选择⽤ RabbitMQ 提供的事务功能,就是⽣产者发送数据之前开启 RabbitMQ 事务channel.txSelect ,然后发送消息,
如果消息没有成功被 RabbitMQ 接收到,那么⽣产者会收到异常报错,此时就可以回滚事务channel.txRollback ,然后重试发送消息;
如果收到了消息,那么可以提交事务channel.txCommit 。
// 开启事务
channel.txSelect
try {
// 这里发送消息
} catch (Exception e) {
channel.txRollback
// 这里再次重发这条消息
}
// 提交事务
channel.txCommit
但是问题是,RabbitMQ 事务机制(同步)⼀搞,基本上吞吐量会下来,因为太耗性能。
所以⼀般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 confirm 模式,在⽣产者那⾥设置开启 confirm 模式之后,
你每次写的消息都会分配⼀个唯⼀的 id,然后如果写⼊了 RabbitMQ 中,RabbitMQ 会给你回传⼀个 ack 消息,告诉你说这个消息 ok 了。
如果RabbitMQ 没能处理这个消息,会回调你的⼀个 nack 接⼝,告诉你这个消息接收失败,你可以重试。
⽽且你可以结合这个机制⾃⼰在内存⾥维护每个消息 id 的状态,如果超过⼀定时间还没接收到这个消息的回调,那么你可以重发。
事务机制和 confirm 机制最⼤的不同在于,事务机制是同步的,你提交⼀个事务之后会阻塞在那⼉,但是 confirm 机制是异步的,
你发送个消息之后就可以发送下⼀个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的⼀个接⼝通知你这个消息接收到了。
所以⼀般在⽣产者这块避免数据丢失,都是⽤ confirm 机制的。
3.1.1 RabbiMQ消息确认机制-可靠抵达
1.保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制。
2.Publisher ConfimCallback:确认模式
3.Publisher ReturnCallback:未投递到Queue,退回模式
4.Consumer Ack:Ack机制
3.1.2 可靠抵达-ConfirmCallback
spring.rabbitmq.publisher-confirms=true
1.在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启confirmcallback 。
2.CorrelationData:用来表示当前消息唯一性。
3.消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有broker 接收到才会调用 confirmCallback。
4.被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。
3.1.2 可靠抵达-ReturnCallback
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
1.confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到
return 退回模式。
2.这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。
3.2 RabbitMQ弄丢了数据
就是 RabbitMQ ⾃⼰弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写⼊之后会持久化到磁盘,哪怕是 RabbitMQ ⾃⼰挂了,
恢复之后会⾃动读取之前存储的数据,⼀般数据不会丢。除⾮极其罕⻅的是,RabbitMQ 还没持久化,⾃⼰就挂了,可能导致少量数据丢失,
但是这个概率较⼩。设置持久化有两个步骤:
- 创建 queue 的时候将其设置为持久化。这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue ⾥的数据的。
- 第⼆个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才⾏,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个 queue ⾥的数据。
注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有⼀种可能,就是这个消息写到了RabbitMQ 中,但是还没来得及持久化到磁盘上,
结果不巧,此时 RabbitMQ 挂了,就会导致内存⾥的⼀点点数据丢失。所以,持久化可以跟⽣产者那边的 confirm 机制配合起来,
只有消息被持久化到磁盘之后,才会通知⽣产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,⽣产者收不到 ack ,
你也是可以⾃⼰重发的。
3.3 消费端弄丢了数据
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,⽐如重启了,那么就尴尬了,
RabbitMQ 认为你都消费了,这数据就丢了。这个时候得⽤ RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的⾃动ack ,
可以通过⼀个 api 来调⽤就⾏,然后每次你⾃⼰代码⾥确保处理完的时候,再在程序⾥ack ⼀把。这样的话,如果你还没处理完,
不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
消费者获取到消息,成功处理,可以回复Ack给Broker
1.basic.ack用于肯定确认;broker将移除此消息
2.basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量
3.basic.reject用于否定确认;同上,但不能批量
默认自动ack,消息被消费者收到,就会从broker的queue中移除
queue无消费者,消息依然会被存储,直到消费者消费
1.消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式
2.消息处理成功,ack(),接受下一个消息,此消息broker就会移除
3.消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack,消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人
3.4 案例Demo
rabbitmq:
host: 192.168.56.10
port: 5672
virtual-host: /
# 开启发送端确认
publisher-confirms: true
# 开启发送端消息抵达队列的确认
publisher-returns: true
# 只要抵达队列,以异步方式优先回调我们这个returnconfirm
template:
mandatory: true
# 手动ack消息
listener:
simple:
acknowledge-mode: manual
@Configuration
public class MyRabbitConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public Jackson2JsonMessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1.服务器Broker收到消息就回调
* 1.1spring.rabbitmq.publisher-confirms=true
* 1.2 设置确认回调
*
* 2.消息正确抵达队列进行回调
* 2.1 spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
* 2.2 设置确认回调ReturnCallback
*
* 3.消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)
* spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动签收
* 3.1 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
* 问题: 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失
* 消费者手动确认模式: 只要我们没有明确告诉MQ,货物被签收,没有Ack,消息就一直是unacked状态。
* 即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
* 3.2 如何签收
* channel.basicAck(deliveryTag, false); 签收;业务成功完成就应该签收
* channel.basicNack(deliveryTag, false);拒签;业务失败,拒签
*/
@PostConstruct // MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 1.只要消息抵达Broker就ack=true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
/**
* 1.做好消息确认机制(publisher, consumer[手动ACK])两端确认
* 2.每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
*/
//服务器收到了
//修改消息的状态
System.out.println("confirm...correlationData[" + correlationData + "]==>ack[" + ack + "]==>cause[" + cause + "]");
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
*
* @param message 投递失败的消息详细信息
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exchange 当时这个消息发给哪个交换机
* @param routingKey 当时这个消息用哪个路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//报错误了。修改数据库当前消息的状态->错误,相当于没收到这条消息,后期再进行重发
System.out.println("Fail Message[" + message + "]==>replyCode" + replyCode + "]==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey + "]");
}
});
}
}
@RestController
public class RabbitController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMq")
public String sendMq(@RequestParam(value = "num", defaultValue = "10") Integer num) {
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setName("哈哈-" + i);
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity, new CorrelationData(UUID.randomUUID().toString()));
} else {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
//路由键不存在,模拟投递给队列失败,new CorrelationData(UUID.randomUUID().toString())消息的唯一id
rabbitTemplate.convertAndSend("hello-java-exchange", "hello1.java", orderEntity, new CorrelationData(UUID.randomUUID().toString()));
}
}
return "ok";
}
}
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/**
* queues: 声明需要监听的所有队列
* org.springframework.amqp.core.Message
* 参数 可以写一下类型
* 1.Message message: 原生消息详细信息。头+体
* 2.T<发送的消息的类型> OrderReturnApplyEntity content
* 3.Channel channel: 当前传输数据的通道
* <p>
* Queue: 可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1.订单服务启动多个,都有此代码,是只有一个服务收到消息,还是都收到消息
* 同一个消息,只能有一个客户端收到
* 2.只有一个消息完全处理完,方法运行结束,我们才可以接收到下一个消息
*
* @param message
*/
//@RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler
public void receiveMessage(Message message, OrderReturnReasonEntity content, Channel channel) throws InterruptedException {
//Body:'{"id":1,"orderId":null,"skuId":null,"orderSn":null,"createTime":1617012055776}
System.out.println("接收到消息...内容: " + message + "--->内容: " + content);
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
//System.out.println("接收到消息...内容: " + message + "--->类型: " + message.getClass());
System.out.println("消息处理完成--> " + content.getName());
//channel内按顺序自增的
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag==>" + deliveryTag);
//签收货物,非批量模式multiple:是否批量确认,false做一个签收一个
try {
if (deliveryTag % 2 == 0) {
channel.basicAck(deliveryTag, false);
System.out.println("签收了货物..." + deliveryTag);
} else {
//退货 requeue=false requeue=true 发回服务器,服务器重新入队
//long deliveryTag: 拒收哪一个, boolean multiple: 是否批量拒绝, boolean requeue: 拒收以后要不要重新发回mq
channel.basicNack(deliveryTag, false, true);
//long deliveryTag, boolean requeue
//channel.basicReject();
System.out.println("没有签收货物..." + deliveryTag);
}
} catch (IOException e) {
//网络中断
e.printStackTrace();
}
}
@RabbitHandler
public void receiveMessage1(Message message, OrderEntity content, Channel channel) throws InterruptedException {
//Body:'{"id":1,"orderId":null,"skuId":null,"orderSn":null,"createTime":1617012055776}
System.out.println("接收到消息...内容: " + message + "--->内容: " + content.getOrderSn());
}
}