目录
1. 重试机制
在消息传递的过程中,可能会遇到各种问题,如网路故障,服务不可用,资源不足等,这些问题可能导致消息处理失败,为了解决这些问题,RabbitMQ 提供了重试机制,允许消息在处理失败后重新发送,但是如果程序逻辑引起的错误,呢么重试多次也是无用的,可以设置重置次数
1.1 重试配置
spring:
rabbitmq:
addresses: amqp://lk:lk@44.34.51.65:5672/order
listener:
simple:
acknowledge-mode: auto
retry:
enabled: true # 开启消费者失败重试
initial-interval: 5000ms # 初始失败等待时⻓为5秒
max-attempts: 5 # 最⼤重试次数(包括⾃⾝消费的⼀次)
1.2 配置交换机和队列
//重试机制
public static final String RETRY_QUEUE = "retry.queue";
public static final String RETRY_EXCHANGE = "retry.exchange";
//重试机制
@Bean("retryQueue")
public Queue retryQueue() {
return QueueBuilder.durable(Constants.RETRY_QUEUE).build();
}
@Bean("retryExchange")
public DirectExchange retryExchange() {
return ExchangeBuilder.directExchange(Constants.RETRY_EXCHANGE).build();
}
@Bean("retryBinding")
public Binding retryBinding(@Qualifier("retryExchange") Exchange exchange,@Qualifier("retryQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("retry").noargs();
}
1.3 发送消息
@RequestMapping("/retry")
public String retry() {
rabbitTemplate.convertAndSend(Constants.RETRY_EXCHANGE,"retry","retry test");
return "发送成功";
}
1.4 消费消息
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void ListenerQueue(Message message) throws UnsupportedEncodingException {
System.out.printf("接收到的消息: %s,delivery: %d",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
//模拟处理失败
int num = 1 / 0;
System.out.println("处理完成");
}
}
1.5 运行程序
可以看到,在重试了 5 次之后抛出了异常,上述的 delivery 一直是 1,没有变化,说明消息已经到达,此时重试时 delivery 是不会变化的
下面对异常进行捕获
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void ListenerQueue(Message message) throws Exception {
System.out.printf("接收到的消息: %s,delivery: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
//模拟处理失败
try {
int num = 1 / 0;
System.out.println("处理完成");
}catch (Exception e) {
System.out.println("处理失败");
}
}
}
1.6 手动确认
@Component
public class RetryListener {
@RabbitListener(queues = Constants.RETRY_QUEUE)
public void ListenerQueue(Message message, Channel channel) throws Exception {
long delivery = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("接收到的消息: %s,delivery: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
//模拟处理失败
int num = 1 / 0;
System.out.println("处理完成");
channel.basicAck(delivery,false);
}catch (Exception e) {
channel.basicNack(delivery,false,true);
}
}
}
可以看到手动确认模式时,重试次数是无效的,因为消息不断地入队列,此时 delivery 是不断递增的
自动确认模式下,RabbitMQ 会在消息被投递给消费者后自动确认消息,如果消费者处理消息时抛出异常,RabbitMQ 会根据配置重试参数自动将消息重新入队,从而实现重试
手动确认模式下,消费者需要手动对消息进行确认,如果消费者在处理消息时发生异常,可以选择不确认消息使消息重新入队
注意:
1)自动确认模式下:程序逻辑异常,多次重试还是失败,消息会被自动确认,从而导致消息丢失
2)手动确认模式下:程序逻辑异常,多次重试消息依然处理失败,无法被确认,就一直是 unacked 的状态,导致消息积压
2. TTL
TTL(Time to live,过期时间),RabbitMQ 可以消息和队列设置 TTL,当消息到达存活时间之后,还没有被消费,就会被自动删除
2.1 设置消息的 TTL
有两种方法可以设置消息的 TTL
1)设置队列的 TTL,队列中所有消息都有相同的过期时间
2)对消息本身进行单独设置,每条消息的 TTL 可以不同,如果两种方法一起使用,则消息的 TTL 以两者之间较小的数值为准
//TTL
public static final String TTL_QUEUE = "ttl.queue";
public static final String TTL_EXCHANGE = "ttl.exchange";
//TTL
@Bean("ttlQueue")
public Queue ttlQueue() {
return QueueBuilder.durable(Constants.TTL_QUEUE).build();
}
@Bean("ttlExchange")
public DirectExchange ttlExchange() {
return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();
}
@Bean("ttlBinding")
public Binding ttlBinding(@Qualifier("ttlExchange") Exchange exchange,@Qualifier("ttlQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
}
@RequestMapping("/ttl")
public String ttl() {
String ttlTime = "10000"; //10s
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl","ttl test",message -> {
message.getMessageProperties().setExpiration(ttlTime);
return message;
});
return "发送成功";
}
运行程序,观察结果
经过 10s 之后消息被删除,如果不设置 TTL,则表示消息不会过期,如果设置 TTL 为0,则表示除非直接将此消息投递到消费者,否则该消息会被立刻丢弃
2.2 设置队列的 TTL
设置队列 TTL 的方法是在创建队列时,加入 x-message-ttl 参数实现的
配置队列和绑定关系
public static final String TTL_QUEUE2 = "ttl.queue2";
@Bean("ttlQueue2")
public Queue ttlQueue2() {
return QueueBuilder.durable(Constants.TTL_QUEUE2).ttl(10000).build();
}
@Bean("ttlBinding2")
public Binding ttlBinding2(@Qualifier("ttlExchange") Exchange exchange,@Qualifier("ttlQueue2") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("ttl2").noargs();
}
@RequestMapping("/ttl")
public String ttl() {
//发送不带 ttl 的消息
rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl2","ttl test");
return "发送成功";
}
10s 之后刷新页面,发现消息被删除
2.3 两者的区别
设置队列的 TTL ,一旦消息过期,就会从队列中删除
设置消息的 TTL,即使消息过期,也不会立刻从队列中删除,而是在即将投递到消费者之前进行判定的
设置队列的 TTL,队列中已过期的消息肯定在队列头部,RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可
而设置消息的 TTL,由于每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,如果等到该消息及激昂被消费时在判定是否过期,如果过期再进行删除即可
3. 死信队列
3.1 死信的概念
死信就是因为各种原因无法被消费的信息,当消息再一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX 的队列,称为死信队列
消息变成死信一般是因为以下几种情况:
1)消息被拒绝(Basic.Reject / Basic.Nack),并且设置参数为 false
2)消息过期
3)队列达到最大长度
3.2 代码示例
1)声明交换机和队列
//正常队列和交换机
public static final String NORMAL_QUEUE = "normal.queue";
public static final String NORMAL_EXCHANGE = "normal.exchange";
//死信队列和交换机
public static final String DLX_QUEUE = "dLX.queue";
public static final String DLX_EXCHANGE = "dLX.exchange";
//正常
@Bean("normalQueue")
public Queue normalQueue() {
return QueueBuilder.durable(Constants.NORMAL_QUEUE).build();
}
@Bean("normalExchange")
public DirectExchange normalExchange() {
return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();
}
@Bean("normalBinding")
public Binding normalBinding(@Qualifier("normalExchange") Exchange exchange,@Qualifier("normalQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
}
//死信
@Bean("dlQueue")
public Queue dlQueue() {
return QueueBuilder.durable(Constants.DLX_QUEUE).build();
}
@Bean("dlExchange")
public DirectExchange dlExchange() {
return ExchangeBuilder.directExchange(Constants.DLX_EXCHANGE).build();
}
@Bean("dlBinding")
public Binding dlBinding(@Qualifier("dlExchange") Exchange exchange,@Qualifier("dlQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("dl").noargs();
}
2)正常队列绑定死信交换机
当这个队列中存在死信时,RabbitMQ 会自动地把这个消息重新发布到设置地 DLX 上,进而被路由到另一个队列,即死信队列,可以监听这个死信队列中的消息进行相应的处理
@Bean("normalQueue")
public Queue normalQueue() {
return QueueBuilder.durable(Constants.NORMAL_QUEUE)
.deadLetterExchange(Constants.DLX_EXCHANGE)
.deadLetterRoutingKey("dl")
.ttl(10000)
.maxLength(10L)
.build();
}
3)发送消息
@RequestMapping("/dl")
public String dl() {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test");
return "发送成功";
}
4)运行程序,观察结果
可以看到过了 10s 之后消息进入了死信队列
接下来测试 20 条数据
@RequestMapping("/dl")
public String dl() {
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test" + i);
}
return "发送成功";
}
由于设置了队列的长度为 10,发了 20 条数据,所以发送 20 条消息,就会有 10 条消息立刻进入死信队列,当消息过期之后,正常队列的 10 条消息也会进入死信队列
5)测试消息拒收
@Component
public class DlListener {
@RabbitListener(queues = Constants.NORMAL_QUEUE)
public void ListenerQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("接收到消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
int num = 1 / 0;
channel.basicAck(deliveryTag,false);
}catch (Exception e) {
//设置消息不会重新发送,直接丢弃进入死信队列
channel.basicNack(deliveryTag,false,false);
}
}
@RabbitListener(queues = Constants.DLX_QUEUE)
public void ListenerDLXQueue(Message message,Channel channel) throws Exception{
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.printf("死信队列接收到的消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
}
}
4. 延迟队列
延迟队列(Delayed Queue),即消息被发送以后并不像让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费
RabbitMQ 本身没有支持延迟队列的功能,但是可以通过 TTL + 死信队列的方式组合模拟出延迟队列的功能
假设一个应⽤中需要将每条消息都设置为10秒的延迟,⽣产者通过 normal_exchange 这个交换器将发送的消息存储在 normal_queue 这个队列中,消费者订阅的并非是 normal_queue 这个队列,而是 dlx_queue 这个队列,当消息从 normal_queue 这个队列中过期之后被存入 dlx_queue 这个
队列中,消费者恰好消费了延迟 10s 的这条消息
所以在死信队列章节的就是延迟队列的使用
4.1 TTL + 死信队列实现
继续用死信队列的代码
生产者发送两条消息,第一条 10s 过期,第二条 20s 过期
@RequestMapping("/dl")
public String dl() {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 10s", message -> {
message.getMessageProperties().setExpiration("10000");
return message;
});
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 20s",message -> {
message.getMessageProperties().setExpiration("20000");
return message;
});
System.out.printf("%tc 消息发送成功 \n",new Date());
return "发送成功";
}
@RabbitListener(queues = Constants.DLX_QUEUE)
public void ListenerDLXQueue(Message message,Channel channel) throws Exception{
System.out.printf("%tc 死信队列接收到的消息: %s,deliveryTag: %d \n",new Date(),new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
}
可以看到两条消息按照过期时间一次进入了死信队列
4.2 存在问题
把生产消息的顺序修改以下,先发送 20s 的消息,再发送 10s 的消息
@RequestMapping("/dl")
public String dl() {
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 20s",message -> {
message.getMessageProperties().setExpiration("20000");
return message;
});
rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 10s", message -> {
message.getMessageProperties().setExpiration("10000");
return message;
});
System.out.printf("%tc 消息发送成功 \n",new Date());
return "发送成功";
}
这时候发现 10s 过期的消息也是在 20s 后才进入死信队列中,这是因为,RabbitMQ 只会检查队首消息是否过期,如果过期则丢到死信队列中,20s 的消息是在队首,如果第一个消息的延时时间很长,第二个消息的延时很短,那么第二个消息并不会优先执行,因此 TTL + 死信队列实现延迟队列的时候,需要确保每个任务的延迟时间是一致的
5. 事务
RabbitMQ 是基于 AMQP 协议实现的,该协议实现了事务机制,因此 RabbitMQ 也支持事务机制,Spring AMQP 也提供了事务的相关操作,RabbitMQ 事务允许开发者确保消息的发送和接收是原子的,要么全部成功,要么全部失败
5.1 配置事务管理器
@Configuration
public class TransConfig {
@Bean
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
return new RabbitTransactionManager(connectionFactory);
}
@Bean("transRabbitTemplate")
public RabbitTemplate transRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//开启事务
rabbitTemplate.setChannelTransacted(true);
return rabbitTemplate;
}
}
5.2 声明队列
@Configuration
public class RabbitMQConfig {
@Bean("transQueue")
public Queue transQueue() {
return QueueBuilder.durable(Constants.TRANS_QUEUE).build();
}
}
5.3 生产者
@RequestMapping("/producer")
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate transRabbitTemplate;
@Transactional
@RequestMapping("/trans")
public String trans() {
transRabbitTemplate.convertAndSend("", Constants.TRANS_QUEUE,"trans test1");
int num = 1 / 0;
transRabbitTemplate.convertAndSend("", Constants.TRANS_QUEUE,"trans test2");
return "发送成功";
}
}
不添加 @Transactional,会发现消息 1 发送成功
添加 @Transactional,消息 1 和 2 全部发送失败
6. 消息分发
6.1 概念
RabbitMQ 队列有多个消费者时,队列会把收到的消息分派给不同的消费者,每条消息只会发送给订阅列表的一个消费者,如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可
默认情况下,RabbitMQ 是以轮询的方式分发的,如果某些消费者消费速度慢,而某些消费者消费速度快,就可能导致某些消费者消息积压,某些消费者空闲,从而导致整体的吞吐量下降
此时就可以使用 channel.basicQos(int prefetchCount) 方法来限制当前信道上的消费者所能保持最大未确认消息的数量
例如消费端调用了 channel.basicQos(5),RabbitMQ 会为该消费者计数,发送一条消息计数 +1,消费一条消息计数 -1,当达到了设定的上限,RabbitMQ 就不会在向它发送消息了,直到消费者确认了某条消息,类似于 TCP 中的滑动窗口,prefetchCount 设置为0时表示没有上限
6.2 应用场景
消息分发的常见应用场景如下:
1)限流
2)非公平分发
6.2.1 限流
例如订单系统每秒最多处理 5000 请求,在正常情况下,订单系统可以满足需求,但是当请求瞬间增多,每秒 1w 个请求时,这些请求全部发送 MQ 订单系统中,会把系统压垮
RabbitMQ 提供了限流机制,可以控制消费端一次只拉取 N 个请求,通过设置 prefetchCount 参数,同时必须要设置消息应答方式为手动应答
prefetchCount:控制消费者从队列中预取(prefetch)消息的数量,以此来实现流控制和负载均衡
代码示例:
1)配置 prefetch,设置应答方式为手动应答
listener:
simple:
acknowledge-mode: manual
prefetch: 5
2)配置交换机和队列
//限流
public static final String QOS_QUEUE = "qos.queue";
public static final String QOS_EXCHANGE = "qos.exchange";
//限流
@Bean("qosQueue")
public Queue qosQueue() {
return QueueBuilder.durable(Constants.QOS_QUEUE).build();
}
@Bean("qosExchange")
public DirectExchange qosExchange() {
return ExchangeBuilder.directExchange(Constants.QOS_EXCHANGE).build();
}
@Bean("qosBinding")
public Binding qosBinding(@Qualifier("qosExchange") Exchange exchange, @Qualifier("qosQueue") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("qos").noargs();
}
3)发送消息,一次发送 20 条消息
@RequestMapping("/qos")
public String qos() {
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(Constants.QOS_EXCHANGE,"qos","qos test" + i);
}
return "发送成功";
}
4)消费者监听
@Component
public class QosListener {
@RabbitListener(queues = Constants.QOS_QUEUE)
public void ListenerQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
//消费者不进行手动确认
//channel.basicAck(deliveryTag,false);
}catch (Exception e) {
channel.basicNack(deliveryTag,false,true);
}
}
}
运行程序,观察结果
可以看到,ready,也就是待发送 15 条,未确认 5 条(因为代码没有手动 ack)
将 prefetch: 5 注掉,再次观察结果
6.2.2 负载均衡
也可以使用此配置来实现"负载均衡"
例如,在有两个消费者的情况下,一个消费者处理任务非常快,另一个非常慢,就会造成一个消费者很忙,另一个很闲,此时可以设置 prefetch=1 的方式,告诉 RabbitMQ 一次只给一个消费者一条消息,也就是说,在处理并确定一条消息之前,不要向该消费者发送新消息,相反,将任务派发给下一个不忙的消费者
代码示例:
1)配置 prefetch,设置应答方式为手动应答
listener:
simple:
acknowledge-mode: manual
prefetch: 1
2)启动两个消费者
@Component
public class QosListener {
@RabbitListener(queues = Constants.QOS_QUEUE)
public void ListenerQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("消费者1接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
channel.basicAck(deliveryTag,false);
}catch (Exception e) {
channel.basicNack(deliveryTag,false,true);
}
}
@RabbitListener(queues = Constants.QOS_QUEUE)
public void ListenerQueue2(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("消费者2接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
message.getMessageProperties().getDeliveryTag());
//模拟处理流程慢
Thread.sleep(100);
channel.basicAck(deliveryTag,false);
}catch (Exception e) {
channel.basicNack(deliveryTag,false,true);
}
}
}
运行程序,观察结果
可以明显的看到消费者 2 处理的消息比较少
上述 deliveryTag 有重复是因为两个消费者使用的是不同的 Channel,每个 Channel 上的deliveryTag 都是独立计数的