什么是 rabbitmq
采用 AMQP 高级消息队列协议的一种消息队列技术,最大的特点就是消费并不需要确保提供方存在,实现了服务之间的高度解耦
消息队列的作用与使用场景
异步:批量数据异步处理(批量上传文件)
削峰:高负载任务负载均衡(电商秒杀抢购)
解耦:串行任务并行化(退货流程解耦)
广播:基于Pub/Sub实现一对多通信
消息如何路由?
消息 -> 交换器 -> 队列,消息携带Routing-key至交换器。交换器根据交换器的路由规则和交换器和队列的Binding-key绑定关系,将消息路由至指定的队列。常见的交换器的路由规则有3种:
- fanout:如果交换器收到消息,将会广播到所有绑定的队列上
- direct:如果路由键完全匹配,消息就被投递到相应的队列
- topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符
多个消费者监听一个队列时,消息如何分发
- 轮询,默认的策略,消费者轮流,平均地接收消息
- 公平分发,根据消费者的能力来分发消息,给空闲的消费者发送更多消息
//当消费者有x条消息没有响应ACK时,不再给这个消费者发送消息
channel.basicQos(int x)
消息在什么时候会变成死信
- 消息拒绝并且没有设置重新入队
- 消息过期
- 消息堆积,并且队列达到最大长度,先入队的消息会变成DL
死信 queue 可用于排查 message 被 reject 或 undeliver 的原因。
消息如何被优先消费
//生产者
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-max-priority",10);
//消费者
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.priority(5) // 优先级,默认为5,配合队列的 x-max-priority 属性使用
如何自动删除长时间没有消费的消息
// 通过队列属性设置消息过期时间
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-message-ttl",6000);
// 对每条消息设置过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("10000") // TTL
消息基于什么传输
由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。
RabbitMQ上的一个queue中存放的message是否有数量限制?
可以认为是无限制,因为限制取决于机器的内存,但是消息过多会导致处 理效率的下降。
普通集群模式下,若cluster中拥有某个queue的owner node失效了,且该queue 被声明具有durable属性,是否能够成功从其他node上重新声明该 queue ?
不能,在这种情况下,将得到404 NOT_FOUND错误。只能等queue所 属的node恢复后才能使用该queue。但若该queue本身不具有durable 属性,则可在其他node上重新声明。
routing_key和binding_key的最大长度是多少?
255字节
如何确保证消息不丢失
1. 持久化
消息持久化,当然前提是队列必须持久化。
RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit会在消息提交到日志文件后才发送响应。
一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么Rabbit会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。
2. 生产者端
rabbitTemplate.setConfirmCallback()回调方法,检测消息是否成功发送到Exchange。ack=true成功,为保证消息的可靠传输,这里可以记录消息的状态为已发送成功;ack=false失败,为保证不丢失消息,这里可以将未成功发送的消息记录下来,起异步任务重新发送。
rabbitTemplate.setMandatory(true)+rabbitTemplate.setReturnCallback()这两项一起设置,检测消息是否从Exchange路由到Queue。这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法,为保证不丢失消息,这里可以将未成功发送的消息记录下来,查看消息被丢弃的原因;不设置的话,找不到路由队列的消息会被直接丢弃;
生产者确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
3. 消费者端
spring.rabbitmq.listener.simple.acknowledge-mode=manual,设置mq消费消息手动确认。
当消费者成功消费一条消息之后,channel.basicAck(long deliveryTag, boolean multiple),进行手动ack,然后mq才会将此条消息从队列中删除。注意,如果消费者忘记手动ack,则消息会一直滞留在mq,大bug;这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。保证数据的最终一致性;
如果消费者处理消息失败,channel.basicNack(long deliveryTag, boolean multiple, boolean requeue),消费者发送nack,可以指定消息是否requeue重新入队列;
下面罗列几种特殊情况:
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,占用消费者的Qos, 将不会给该消费者分发更多的消息。
如何确保证消息顺序消费
从根本上说,异步消息是不应该有顺序依赖的。在MQ上估计是没法解决。要实现严格的顺序消息,简单且可行的办法就是:保证生产者 - MQServer - 消费者是一对一对一的关系。
如果有顺序依赖的消息,要保证消息有一个hashKey,类似于数据库表分区的的分区key列。可以根据这个hashKey对消息进行分组,使得具有依赖关系的消息具有相同的hashKey,路由至同一个队列,然后单个consumer消费保障消息的顺序消费。具体方法有:
- 在生产者端,根据消息的hashKey进行分组,确保有依赖关系的消息分到同一个组。每个组对应一个 queue ,一个 consumer 消费一个 queue,使得消息顺序消费;
- 在消费者端,消息全部路由到一个 queue,一个consumer 先把所有的消息接收,然后这个 consumer 内部用内存队列根据消息的hashKey对消息做分组排队,然后起多线程来消费不同内存队列中的消息;
如何规避消息重复消费
1. 消息消费的业务逻辑实现幂等性,消息多次消费业务结果保持一直;
2. 通过业务 messageId 作为消息的标识,如果消息已经消费过,同一个ID消息在此到来时直接丢弃消息;