Rabbitmq的使用五_消息的可靠性投递
官网地址:https://www.rabbitmq.com/tutorials/tutorial-seven-java.html
通过前面的学习,可以知道消息的可靠性投递,可以做持久化操作
过程一:从生产者发送消息到RabbitMQ服务器的过程。
过程二:确保消息从交换机路由到队列
过程三:确保消息在队列中正确的存储
过程四:确保消息从队列正确地投递到消费者
消息本身也要做持久化操作。
2.Rabbitmq的工作模型
执行流程:生产者发送消息通过channel和rabbitmq建立连接,发送一条携带路由关键字的消息到交换机上面的时候。交换机根据消息携带关键字,去查哪一些队列绑定了这些关键字,然后把消息路由到这些队列上面,然后消费者从队列中取消息就可以了
一个rabbitmq里面,可以建立多个不同的交换机和绑定队列,这时就用到我们的virtual host虚拟机。我们可以把每一个虚拟机当做一个rabbitmq的服务器。这样做解决了硬件资源的问题。因此,我们可以创建多个虚拟机,每一个虚拟机都可以看做是一个rabbitmq的服务器,可以根据此来创建很多的交换机和队列,以及定义之间的绑定关系,然后我们去创建一些用户,比如我们的资金系统,有资金系统的虚拟机,我们把资金系统的用户分配资金系统的权限就ok了。那么对应的其他系统,每一个系统都有自己的用户,自己的权限,那么不同的虚拟机之间是完全透明的。他们之间可以建立同名的交换机,通过这样的方式,我们可以实现硬件资源的高效利用和硬件资源的隔离。
生产者或者消费者和rabbitmq之间的连接是一个长连接。如果我们直接连接,频繁的创建连接,就会造成性能问题,我们引入一个channel的通道。这是一些虚拟的连接。我们需要连接rabbitmq的话,直接从这些虚拟的连接中拿一个连接就可以使用了。
exchange:本质是地址的清单,本身不存储消息。相当于一个路由功能
引入交换机的作用:是为了达到消息的一个灵活的投递
能够创建多少队列:这个主要取决于队列是保存在哪里的,如果队列是保存在内存里的,那么创建多少队列,取决于内存空间的大小,如果队列是保存在硬盘上的,取决于硬盘空间的大小
如何实现消息的灵活投递:交换机和队列之间的关系是多对多的关系
2.1如果过程一发生了异常怎么办?
从生产者发送消息到rabbitmq服务器的过程中,失败了。RabbitMQ给我们提供了两种方式:1.AMQP事务 2.Confirm模式
1.事务模式:每一个事务都要等待消息的一个应答,所以事务模式极其消耗性能的一个东西。因为极其消耗性能,所以在生产中一般也不会使用,因为,如果每一条消息,都开启事务,太消耗性能了。
事务模式的三个方法:channel.txSelect(); channel.txCommit(); channel.txRollback();
代码案例:
public classRabbitMqTransActionSender {//事务队列
private static final String TRANSTATION_QUEUE = "transaction_queue";public static void main(String[] args) throwsIOException, TimeoutException {//1.获取连接
Connection connection =RabbitMQConnectionFactory.getRabbitMQConnections();//2.获取通道
Channel channel =connection.createChannel();//3.声明队列
channel.queueDeclare(TRANSTATION_QUEUE, false, false, false, null);//4.发送消息
String msg = "小河流水哗啦啦,我和姐姐去采花1";//将channel设置为事务模式
try{
channel.txSelect();
channel.basicPublish("", TRANSTATION_QUEUE, null, msg.getBytes());int a = 10 / 0;
channel.txCommit();
System.out.println("消息已经提交");
}catch(Exception e) {
channel.txRollback();
System.out.println("消息已经回滚");
}
channel.close();
connection.close();
}
}
View Code
演示正常情况,发送者发送消息,消费者正常接收消息
消息发送过程中,出现异常。消息回滚掉,消费者不会接收到任何消息
图4生产者 图5消费者
2.确认模式
当我们发送消息成功之后,会有一个ack应答,只要我们的channel.waitForConfirms返回一个true,表示我们的消息就是发送成功的。
rabbitmq的消息确认,默认不启动了,需要开启
Channel channel =connection.createChannel();
channel.confirmSelect();
单条发送消息
当发送者发送消息报错时,消费者就不会受到消息
public classRabbitMqConfirmSender {private static final String CONFIRM_QUEUE = "confirm_queue";public static void main(String[] args) throwsIOException, TimeoutException, InterruptedException {//1.获取连接
Connection connection =RabbitMQConnectionFactory.getRabbitMQConnections();//2.获取通道
Channel channel =connection.createChannel();//3.声明队列
channel.queueDeclare(CONFIRM_QUEUE, false, false, false, null);//4.发送消息
String msg = "小河流水哗啦啦,我和姐姐去采花1";//将channel设置为confirm模式
channel.confirmSelect();
channel.basicPublish("", CONFIRM_QUEUE, null, msg.getBytes());if(channel.waitForConfirms()) {
System.out.println("消息发送成功");
}
channel.close();
connection.close();
}
}
View Code
我们在开始时提到,代理异步确认已发布的消息,代码会同步等待,直到消息确认为止。客户端实际异步接收确认,并解除调用阻塞,可以看作是一个在内部依赖于异步通知的同步助手。
单条消息的发送确认模式,效率也是很低的,每一条消息,先开启消息确认机制,然后发送,然后处理消息应答,效率太低。
批量发送消息
public classRabbitMqConfirmBatchSender {private static final String BATCH_CONFIRM_QUEUE = "batch_confirm_queue";public static void main(String[] args) throwsIOException, TimeoutException, InterruptedException {//1.获取连接
Connection connection =RabbitMQConnectionFactory.getRabbitMQConnections();//2.获取通道
Channel channel =connection.createChannel();//3.声明队列
channel.queueDeclare(BATCH_CONFIRM_QUEUE, false, false, false, null);//4.发送消息//将channel设置为confirm模式
channel.confirmSelect();
String msg= "小河流水哗啦啦,我和姐姐去采花";for (int i = 1; i <= 100; i++) {//int a = 19/0;
channel.basicPublish("", BATCH_CONFIRM_QUEUE, null, (msg +i).getBytes());
}if(channel.waitForConfirms()) {
System.out.println("消息发送成功");
}
channel.close();
connection.close();
}
}
View Code
当批量发送异常的时候,消费者不会收到任何消息,如下图所示:
这种批量发送消息的方式,只要有一条消息未被broker确认,就会发生异常,也就是说当我们channel.waitFroConfirms;只要不抛出异常,就可以认为我们的消息发送成功了
但是:这种方式也有缺点:
1.我们是积累多少条消息进行消息的发送
2.我们假设是1000条发送消息一次,如果前999条发送失败,刚好第1000条发送失败了,怎么办
Rabbitmq官网还给我们提供了一种方式,是采用异步的方式,进行消息的收发,使用异步的方式,是可以进行一边发送,一边确认的方式,进行消息的收发的,异步的情况,消息不会自动重发的
Broker异步确认:只需要在发送者客户端注册一个异步回调,就可以接收到确认消息
代码如下:
public classRabbitMqAsyncConfirmSender {private static final String ASYNC_CONFIRM_QUEUE = "async_confirm_queue";public static void main(String[] args) throwsIOException, TimeoutException, InterruptedException {//1.获取连接
Connection connection =RabbitMQConnectionFactory.getRabbitMQConnections();//2.获取通道
Channel channel =connection.createChannel();//3.声明队列
channel.queueDeclare(ASYNC_CONFIRM_QUEUE, false, false, false, null);//4.发送消息
String msg = "小河流水哗啦啦,我和姐姐去采花";//将channel设置为confirm模式
channel.confirmSelect();//发送消息
for (int i = 1; i <= 20; i++) {//if (i==17){//int a = 10/0;//}
channel.basicPublish("", ASYNC_CONFIRM_QUEUE, null, (msg + "======>" +i).getBytes());
}
channel.addConfirmListener(newConfirmListener() {
@Overridepublic void handleAck(long deliveryTag, boolean multiple) throwsIOException {
System.out.print("已确认的消息,标识:" +deliveryTag);
System.out.println("多个消息: " +multiple);
}
@Overridepublic void handleNack(long deliveryTag, boolean multiple) throwsIOException {
System.out.println("Broker 未确认消息,标识: " +deliveryTag);
}
});
System.out.println("程序执行完成");
}
}
View Code
执行结果如下:
在某些应用程序中,确保已发布的消息到达broker可能非常重要。发布者确认是RabbitMQ的一个特性,有助于满足这一需求。发布者确认在本质上是异步的,但是也可以同步地处理它们。
1.单条发布消息,同步等待确认:简单,但吞吐量非常有限。
2.批量发布消息,同步等待批处理的确认:简单、合理的吞吐量,但是很难判断什么时候出现了错误
3.异步处理:最佳的性能和资源的使用,在错误的情况下良好的控制。
2.2 过程二发送失败了怎么办?
就是交换机路由消息到队列的过程中发送失败了。
解决办法:可以给当前交换机设置备份交换机
交换机本身也是支持持久化操作的。在声明交换机的时候,参数三表示是否持久化
/*** Actively declare a non-autodelete exchange with no extra arguments
*@seecom.rabbitmq.client.AMQP.Exchange.Declare
*@seecom.rabbitmq.client.AMQP.Exchange.DeclareOk
*@paramexchange the name of the exchange
*@paramtype the exchange type
*@paramdurable true if we are declaring a durable exchange (the exchange will survive a server restart)
*@throwsjava.io.IOException if an error is encountered
*@returna declaration-confirm method to indicate the exchange was successfully declared*/Exchange.DeclareOk exchangeDeclare(String exchange, String type,boolean durable) throws IOException;
2.3 过程三失败了怎么办?
当我们没有对消息进行配置的时候,默认是保存在内存中的,消息保存在内存中,不可避免的会出现rabbitmq服务器的重启,宕机等问题,所以,可以对消息做一个消息的持久化的处理。
发送消息的时候,可以给消息设置一个properties的属性,通过配置properties可以配置消息的一个持久化操作。
队列可以做持久化操作、消息也可以做持久化操作。
2.4过程四失败了怎么办?
确保消息从队列正确地投递到消费者ack应答
消费者接收到消息的时候,就会给broker一个应答,broker拿到应答之后,就会从队列中删除这条消息。而不是在方法执行完之后,再给服务器的应答。我们可以手动的执行一个ack应答机制。
2.5 如果消费者处理消息的时候,抛出异常了,生产者怎么知道?
1.消费者回调
1) 发送回执(表示每发送一条消息,都给生产者一条消息回执)
2)生产者提供api(生产者发送消息的时候,保存一条消息入库),消费者调用暴露的api,去修改这条消息的状态。如果我们消息的状态没有发生变更的话,那么我就可以判断有可能消费者在处理消息的时候,发生了问题
2.补偿机制
如果没有发送回执:可能是因为网络问题
如果没有调用api:可能消费者调用生产者的过程中出现了问题
在以上的情况都没有使用的情况下面,我们就需要使用一个补偿机制,比如
1)消息的重发:
重发的前提:我们发送前需要把数据保存到数据库中,然后重发的时候,直接从数据库中进行消息的获取,然后重新进行一个消息的发送。但是如果发送5次,10次,如果消费者一直没有应答。。。就会一直重发。所以这里一定要做一个次数的控制,等达到一定的次数之后,我们就不进行重发操作了,我们会在夜间进行一个对账的操作。
可能发送消息的时候,就是消费成功了,但是由于网络原因,回执执行慢了,
3.消息的幂等性
1)处理1次消息,跟处理10 次消息的结果都是一样的
2) 消息必须有一个唯一性的标志:在金融系统中,任何一笔交易,都会有一个全局流水号的标志, message发送消息的时候,有一个消息的id,消息id+业务id唯一判断 (重帐控制)消费会不会重复消费
3.如果保证消息的顺序消费
多个生产者和多个消费者的情况,基本是没有办法实现一个消息的顺序消费的,比如发送了3条消息,消费者的消费速率是不一样,所以我们无法保证哪一个消息是先消费完成的。所以我们完成消费的顺序性,如果我们只有一个生产者,和一个消费者,根据队列先进先出的思想,我们是可以保证消息的一个顺序消费的。对于每一组消息,我们都有一个parentID(批次号),也有一个seqNo,如果上一个批次的消息没有消费完,就不能消费下一个消息的
int sequenceNumber =channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);
当然如果要保证消息的一个顺序性的消费,我们可以使用rocketmq的顺序消费,他本身就是支持的
4.springboot整合rabbitmq实现事务模式
在SpringBoot项目中,使用RabbitMQ事务,只需要声明一个事务管理的Bean,并将RabbitTemplate的事务设置为true即可
消费者方
1.消费者添加配置文件
server:
port: 8010
spring:
rabbitmq:
host: ip地址
username: yingxiaocao
password: yingxiaocao
virtual-host: /yingxiaocao
listener:
type: simple
simple: # 开启手动应答
acknowledge-mode: manual
配置mq地址
2.声明队列、交换、绑定关系、事务bean(消费者和生产者都要配置。否则当生产者启动,消费者未启动时,发送消息,就会报错)
@PropertySource("classpath:transaction_mq.properties")
@Configurationpublic classConsumerConfig {
@Value("${TRANSACTION_EXCHANGE_NAME}")privateString exchangeName;
@Value("${FIRST_QUEUE}")privateString firstQueue;//1.声明一个交换机
@Bean("fanout_exchange")publicFanoutExchange getFanoutExchange() {
FanoutExchange fanoutExchange= newFanoutExchange(exchangeName);returnfanoutExchange;
}//2.声明2个队列
@Bean("first_queue")publicQueue getFirstQueue() {
Queue queue= newQueue(firstQueue);returnqueue;
}//3.绑定关系
@Beanpublic Binding bindingExchange(@Qualifier("fanout_exchange") FanoutExchange fanoutExchange, @Qualifier("first_queue") Queue queue) {returnBindingBuilder.bind(queue).to(fanoutExchange);
}
@BeanpublicRabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory cachingConnectionFactory) {return newRabbitTransactionManager(cachingConnectionFactory);
}
声明队列、交换机
3.创建一个消费者
@Component
@PropertySource("classpath:transaction_mq.properties")public classRabbitmqConsumer {
@RabbitListener(queues= {"${FIRST_QUEUE}"})public void receive(Message message, Channel channel) throwsIOException {
String msg= newString(message.getBody());
System.out.println("接收到的消息: " +msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
消费者
4.创建一个启动类
@SpringBootApplicationpublic classTransactionConsumerStartApp {public static voidmain(String[] args) {
SpringApplication.run(TransactionConsumerStartApp.class, args);
}
}
消费者启动类
生产者方
1.创建一个controller
@RequestMapping("/producer")
@RestControllerpublic classProducerController {
@AutowiredprivateProducerService producerService;
@RequestMapping("/send")public voidsend(String msg) {
producerService.send(msg);
}
}
controller
2.创建一个service,用来发送消息
@PropertySource("classpath:transaction_mq.properties")
@Servicepublic classProducerService {
@Value("${TRANSACTION_EXCHANGE_NAME}")privateString exchangeName;
@AutowiredprivateRabbitTemplate rabbitTemplate;
@PostConstructpublic voidinit() {//创建对象的同时,开启channel事务模式
rabbitTemplate.setChannelTransacted(true);
}/*** 发送消息
*@parammsg*/@Transactionalpublic voidsend(String msg) {
rabbitTemplate.convertAndSend(exchangeName,"",msg);
System.out.println("消息已发送: "+msg);if (msg.equals("xxx")) {throw new RuntimeException("抛出异常了");
}
}/*** 配置启用rabbitmq事务
*@paramconnectionFactory
*@return
*/@BeanpublicRabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) {return newRabbitTransactionManager(connectionFactory);
}
}
service发送消息
3.创建一个启动类
@SpringBootApplicationpublic classTransactionProducerStartApp {public static voidmain(String[] args) {
SpringApplication.run(TransactionProducerStartApp.class, args);
}
}
View Code
4.声明配置
@PropertySource("classpath:transaction_mq.properties")
@Componentpublic classRabbitConfig {
@Value("${TRANSACTION_EXCHANGE_NAME}")privateString exchangeName;
@Value("${FIRST_QUEUE}")privateString firstQueue;//1.声明一个交换机
@Bean("fanout_exchange")publicFanoutExchange getFanoutExchange() {
FanoutExchange fanoutExchange= newFanoutExchange(exchangeName);returnfanoutExchange;
}//2.声明1个队列
@Bean("first_queue")publicQueue getFirstQueue() {
Queue queue= newQueue(firstQueue);returnqueue;
}//3.绑定关系
@Beanpublic Binding bindingExchange(@Qualifier("fanout_exchange") FanoutExchange fanoutExchange, @Qualifier("first_queue") Queue queue) {returnBindingBuilder.bind(queue).to(fanoutExchange);
}
@BeanpublicRabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory cachingConnectionFactory) {return newRabbitTransactionManager(cachingConnectionFactory);
}
}
声明队列、交换机、绑定关系
执行结果:
正常情况,生产者发送消息,消费者收到消费
异常情况:生产者发送消息后,抛出异常了,消费者并没有收到消息,可见消息回滚了
但是事务模式效率太差。
5.springboot整合rabbitmq实现消息确认模式
通过生产者确认机制,生产者可以在消息被服务器成功接收时得到反馈,并有机会处理未被成功接收的消息。
在Springboot中开启RabbitMQ的生产者确认模式也很简单,只多了一行配置:publisher-confirms: true即表示开启生产者确认模式。
server:
port: 9010
spring:
rabbitmq:
username: yingxiaocao
password: yingxiaocao
host: 地址
virtual-host: /yingxiaocao
listener:
type: simple
publisher-confirms: true # 开启消息确认模式
View Code
改变生产者代码
@PropertySource("classpath:confirm_mq.properties")
@Service
@Slf4jpublic class ProducerService implementsRabbitTemplate.ConfirmCallback {
@Value("${CONFRIM_EXCHANGE_NAME}")privateString exchangeName;
@AutowiredprivateRabbitTemplate rabbitTemplate;
@PostConstructpublic voidinit() {//创建对象的同时,开启channel事务模式//rabbitTemplate.setChannelTransacted(true);//开启确认模式
rabbitTemplate.setConfirmCallback(this);
}/*** 发送消息
*
*@parammsg*/
public voidsend(String msg) {//创建一个消息编号
CorrelationData correlationData = newCorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(exchangeName,"aaa", msg, correlationData);
log.info("消息id{},路由key{},发送消息内容{}", correlationData.getId(), "aaa", msg);//创建一个无法投递成功的消息
CorrelationData correlationData1 = newCorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(exchangeName,"bbb", msg, correlationData1);
log.info("消息id{},路由key{},发送消息内容{}", correlationData1.getId(), "bbb", msg);
}/*** 消息回调
*
*@paramcorrelationData
*@paramack
*@paramcause*/@Overridepublic void confirm(CorrelationData correlationData, booleanack, String cause) {
String id= correlationData != null ? correlationData.getId() : "";if(ack) {
log.info("消息投递成功,消息id{}", id);
}else{
log.info("消息投递失败,消息id{},原因:{}", id, cause);
}
}
}
View Code
执行代码结果如下:
生产者截图
消费者截图。
由运行结果可知。消息只要发送到了交换机,不管消息有没有成功投递到队列里面,都会给生产者一个ack应答。
如何让消息被路由到队列后再返回ACK呢?
1.设置mandatory参数
设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
当把 mandotory 参数设置为 true 时,如果交换机无法将消息进行路由时,会将该消息返回给生产者,而如果该参数设置为false,如果发现消息无法进行路由,则直接丢弃。
rabbitTemplate.setMandatory(true);
如果要让消息返回给生产者,需要添加一个回调
为了进行回调,我们需要实现一个接口 RabbitTemplate.ReturnCallback
使用mandatory这种方式,如果消息发送失败,返回给生产者,通过看日志的方式,就比较麻烦了。可以使用备份交换机的方式。如果消息从交换机路由到队列失败,转发给备份交换机,由备份交换机绑定的队列,进行处理。就比较好了
2.设置备份交换机
创建交换机的时候,为交换机添加备份交换机代码,可为备份交换机添加不同的队列,实现不同的功能
.withArgument("alternate-exchange",BUSINESS_BACKUP_EXCHANGE_NAME);