MQ的一些常见问题
消息可靠性
生产者消息确认
返回ack,怎么感觉这么像某个tcp的3次握手。
使用资料提供的案例工程.
在图形化界面创建一个simple.queue的队列,虚拟机要和配置文件里面的一样。
SpringAMQP实现生产者确认
AMQP里面支持多种生产者确认的类型。
simple是同步等待模式,发了消息之后就一直等待结果,可能会导致代码阻塞。
correlated是异步回调模式,像前段的ajax请求的回调函数。
ApplicationContextAware是bean工厂通知。会在Spring容器创建完后来通知并传一个spring容器到下面的方法。然后从中取到rabbitTemplate的bean并设置ReturnCallback。
ReturnCallback:消息到了交换机,路由时失败了没有到达消息队列
ConfirmCallback:消息连交换机都没到。
这个不像ReturnCallback只能配置一个,这个可以在每次发消息时设置。
这里在发送消息时多了一个correlationData,这是在配置开关选择的confirm类型为correlated。里面封装了消息的唯一id和callback.
callback里面的result是成功的回调函数,ex是失败的回调函数。这里的失败是指回调都没收到。
实现
先是在生产者的配置文件里要加上前面的配置j
编写returnCallback
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {//获取RabbitTemplate对象RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//配置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {//记录日志log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());//如果有需要的话,可以重发消息});}
}
编写ConfirmCallback
这里先要在图形界面手动将交换机和消息队列做绑定
@Testpublic void testSendMessage2SimpleQueue() throws InterruptedException {//1.准备消息String message = "hello, spring amqp!";//2.准备correlationData//2.1消息IDCorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());//2.2准备ConfirmCallbackcorrelationData.getFuture().addCallback(result -> {//判断结果if(result.isAck()){//ACKlog.debug("消息成功投递到交换机!消息ID:{}",correlationData.getId());}else{//NACKlog.error("消息投递到交换机失败!消息ID:{}",correlationData.getId());}}, ex -> {//记录日志log.error("消息发送失败!",ex);//重发消息});//3.发送消息rabbitTemplate.convertAndSend("camq.topic", "simple.test", message,correlationData);}
测试得到
成功的测试情况
失败的测试情况
投递交换机失败,交换机不存在
投递队列失败,队列不存在
消息持久化
这里通过重启rabbitmq容器发现消息都不见了可以确认,rabbitmq和redis一样都是内存运行的。
甚至我手动加上的消息队列和绑定关系都不见了。这里消息队列不见是因为前面创建队列时选择的是Transient,不持久化。系统默认的交换机都还在,是因为durable为true,持久化。
创建队列或交换机的时候可以设置Durability为Durable即可持久化。
在消费者代码中进行交换机和队列的创建,然后可以看见如下持久化的交换机和队列.
@Configuration
public class CommonConfig {@Beanpublic DirectExchange simpleExchange(){return new DirectExchange("simple.direct",true,false);}@Beanpublic Queue simpleQueue(){return QueueBuilder.durable("simple.queue").build();}
}
手动发送一条消息进行测试
重启之后消息还是消失了。
要想让消息持久化,需要在发送消息时指定。
@Testpublic void testDurableMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,pop".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.build();//2.发送消息rabbitTemplate.convertAndSend("simple.queue",message);}
重启之后消息就持久化了。
通常在springamqp中这些都是持久化的。
消费者消息确认
在none模式下,消费者拿到消息都就报异常了,然后消息也没了。
在auto模式下,消费者拿到消息后给mq报了个unack,然后消息会重新投递,消费者继续拿消息,tmd,死循环了。 但是这里消息就不会消失了。
@RabbitListener(queues = "simple.queue")public void listenSimpleQueue(String msg) {System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");System.out.println(1/0);log.info("消费者处理消息成功!");}
消费失败重试机制
重试次数耗尽之后会将消息丢弃。
消费者失败消息处理策略
在消费者代码中
@Configuration
public class ErrorMessageConfig {@Beanpublic DirectExchange errorMessageExchange(){return new DirectExchange("error.direct");}@Beanpublic Queue errorQueue(){return new Queue("error.queue",true);}@Beanpublic Binding errorBinding(){return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");}@Beanpublic MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");}
}
重新发送消息进行测试,可以看见重试次数耗尽之后就送到了死信队列了。
在里面将异常的堆栈信息也包含了.
死信交换机
初识死信交换机
区别在于,上一个是消费者失败之后寻找交换机路由到error队列,这个是退回到队列,再指定交换机,最后路由。
TTL
这个的应用场景比如说订单超时未支付然后自动取消。
实现
准备 代码部分
@RabbitListener(bindings = @QueueBinding(value=@Queue(name = "dl.queue",durable = "true"),exchange=@Exchange(name="dl.direct"),key = "dl"))public void listenDlQueue(String msg){log.info("接收到 dl.queue的延迟消息:{}",msg);}
@Configuration
public class TTLMessageConfig {@Beanpublic DirectExchange ttlExchange(){return new DirectExchange("ttl.direct");}@Beanpublic Queue ttlQueue(){return QueueBuilder.durable("ttl.queue").ttl(10000).deadLetterExchange("dl.direct").deadLetterRoutingKey("dl").build();}@Beanpublic Binding simpleBinging(){return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");}
}
测试代码
@Testpublic void testTTLMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.build();//2.发送消息rabbitTemplate.convertAndSend("ttl.direct","ttl",message);//3.记录日志log.info("消息成功发送!");}
10s之后在消费者那里就可以看见
然后这里会以短的优先,5s后消费者就可以收到消息。
延迟队列
1.重装rabbitmq容器
这个插件需要找到mq内部的插件文件夹,所以需要在创建容器的时候进行数据卷挂载。
docker run \-e RABBITMQ_DEFAULT_USER=itcast \-e RABBITMQ_DEFAULT_PASS=123321 \-v mq-plugins:/plugins \--name mq \--hostname mq1 \-p 15672:15672 \-p 5672:5672 \-d \rabbitmq:3.8-management
2.安装DelayExchange插件
官方的安装指南地址为:Scheduling Messages with RabbitMQ | RabbitMQ - Blog
上述文档是基于linux原生安装RabbitMQ,然后安装插件。
2.1.下载插件
RabbitMQ有一个官方的插件社区,地址为:Community Plugins — RabbitMQ
大家可以去对应的GitHub页面下载3.8.9版本的插件,地址为Release v3.8.9 · rabbitmq/rabbitmq-delayed-message-exchange · GitHub这个对应RabbitMQ的3.8.5以上版本。
查看挂载的数据卷.
docker volume inspect mq-plugins
接下来的看着好麻烦,以后看文档吧.
还真的麻烦的一批,真不想再搞这玩意,文件搞来搞去。
不知道为什么,挂载数据卷时一直报错,不能用自己定义的文件夹来挂载。
在消费者中如下声明
@RabbitListener(bindings = @QueueBinding(value=@Queue(name = "delay.queue",durable = "true"),exchange=@Exchange(name="delay.direct",delayed = "true"),key = "delay"))public void listenDelayQueue(String msg){log.info("接收到 delay.queue的延迟消息:{}",msg);}
在生产者中如下定义
@Testpublic void testSendDelayMessage(){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久的.setHeader("x-delay",5000).build();//2.准备correlationDataCorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());//3.发送消息rabbitTemplate.convertAndSend("delay.direct", "delay", message,correlationData);log.info("发送消息成功");}
测试结果如下 成功实现延迟5秒。但是会被报错,理论上说交换机应该立即转发,不会延迟,但是这里的延迟交换机可以帮忙保存消息延迟发送,所以这里才会报错,not_router,消息没有到达队列
为了解决这个报错,需要修改生产者代码
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {//获取RabbitTemplate对象RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);//配置ReturnCallbackrabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {//判断是否是延迟消息if (message.getMessageProperties().getReceivedDelay()>0) {//是一个延迟消息,忽略错误提示return;}//记录日志log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());//如果有需要的话,可以重发消息});}
}
惰性队列
消息堆积问题
问题解决
消费者中声明两个队列。
@Configuration
public class LazyConfig {@Beanpublic Queue lazyQueue(){return QueueBuilder.durable("lazy.queue").lazy().build();}@Beanpublic Queue normalQueue(){return QueueBuilder.durable("normal.queue").build();}
}
测试,准备两个队列之后分别向两个队列发消息。
@Testpublic void testLazyMessage(){for(int i=0;i<1000000;i++){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT) //持久的.build();//3.发送消息rabbitTemplate.convertAndSend("lazy.queue", message);}}@Testpublic void testnormalMessage(){for(int i=0;i<1000000;i++){//1.准备消息Message message = MessageBuilder.withBody("hello,ttl".getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT) //持久的.build();//3.发送消息rabbitTemplate.convertAndSend("normal.queue", message);}}
可以看见惰性队列的消息全部到paged out 刷出磁盘了?????、,为什么非惰性队列的也是刷出磁盘了。
MQ集群
集群个屁,不搞了.