1. 概述
在实际工作中经常有些消息需要在某个事件发生之后或之前的指定时间点才处理,例如:订单在十分钟之内未支付则自动取消,用户注册成功后,如果三天内没有登陆则进行短信提醒等。RabbitMQ中提供了延迟队列来处理延后消息,其内部是有序的。
RabbitMQ中的TTL是一个消息或者队列的属性,表明一条消息或者队列中的所有消息的最大存活时间,单位是毫秒。如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果再TTL设置的时间内没有被消费,则会成为死信。
消息设置TTL和队列设置TTL的区别
- 队列设置了TTL属性,一旦消息过期,就会被队列丢弃(如果配置了死信队列被放入死信队列中)
- 消息设置了TTL属性,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的
死信:生产者将消息投递到broker或者直接到队列中,消费者从队列取出消息进行消费,但某些时候由于特定的原因导致队列中的某些消息无法被消费,这样的消息如果没有后续处理,就变成了死信
死信的来源
- 消息TTL过期
- 队列达到最大长度(队列满了,无法再添加数据到MQ中)
- 消息被拒绝(basic.reject或basic.nack)并且requeue=false
2. 队列TTL实现延时消费
2.1. 队列TTL配置类
@Configuration
public class TtlQueueConfig {
private static final String TTL_QUEUE_EXCHANGE = "ttl-queue-exchange";
private static final String TTL_QUEUE_A = "ttl-queue-A";
private static final String TTL_QUEUE_B = "ttl-queue-B";
private static final String TTL_QUEUE_A_ROUT_KEY = "ttl_queue-A-routing-key";
private static final String TTL_QUEUE_B_ROUT_KEY = "ttl_queue-B-routing-key";
private static final String DEAD_LETTER_EXCHANGE = "dead-letter-exchange";
private static final String DEAD_LETTER_QUEUE = "dead-letter-queue";
private static final String DEAD_LETTER_ROUT_KEY = "dead-letter-routing-key";
@Bean(name = "ttlQueueExchange")
public DirectExchange ttlQueueExchange() {
return new DirectExchange(TTL_QUEUE_EXCHANGE);
}
@Bean(name = "deadLetterExchange")
public DirectExchange deadLetterExchange() {
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
@Bean(name = "ttlQueueA")
public Queue ttlQueueA() {
Map<String, Object> map = new HashMap<>(3);
map.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", DEAD_LETTER_ROUT_KEY);
map.put("x-message-ttl", 10000);
return QueueBuilder.durable(TTL_QUEUE_A).withArguments(map).build();
}
@Bean
public Binding ttlQueueBindingA(@Qualifier("ttlQueueA") Queue queue, @Qualifier("ttlQueueExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(TTL_QUEUE_A_ROUT_KEY);
}
@Bean(name = "ttlQueueB")
public Queue ttlQueueB() {
Map<String, Object> map = new HashMap<>(3);
map.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", DEAD_LETTER_ROUT_KEY);
map.put("x-message-ttl", 40000);
return QueueBuilder.durable(TTL_QUEUE_B).withArguments(map).build();
}
@Bean
public Binding ttlQueueBindingB(@Qualifier("ttlQueueB") Queue queue, @Qualifier("ttlQueueExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(TTL_QUEUE_B_ROUT_KEY);
}
@Bean(name = "deadLetterQueue")
public Queue deadLetterQueue() {
return new Queue(DEAD_LETTER_QUEUE);
}
@Bean
public Binding deadLetterBinding(@Qualifier("deadLetterQueue") Queue queue, @Qualifier("deadLetterExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_ROUT_KEY);
}
}
2.2. 队列TTL生产者类
@RestController
@RequestMapping("/ttl")
public class TtlQueueProducerController {
private static final String TTL_QUEUE_EXCHANGE = "ttl-queue-exchange";
private static final String TTL_QUEUE_A_ROUT_KEY = "ttl_queue-A-routing-key";
private static final String TTL_QUEUE_B_ROUT_KEY = "ttl_queue-B-routing-key";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message) {
String uuid1 = UUID.randomUUID().toString();
CorrelationData correlationData1 = new CorrelationData(uuid1);
rabbitTemplate.convertAndSend(TTL_QUEUE_EXCHANGE, TTL_QUEUE_A_ROUT_KEY, "ttl消息:" + message + "为10s", correlationData1);
String uuid2 = UUID.randomUUID().toString();
CorrelationData correlationData2 = new CorrelationData(uuid2);
rabbitTemplate.convertAndSend(TTL_QUEUE_EXCHANGE, TTL_QUEUE_B_ROUT_KEY, "ttl消息:" + message + "为40s", correlationData2);
}
}
2.3. 死信消费者类
@Component
public class DeadLetterQueueConsumer {
private static final String DEAD_LETTER_QUEUE = "dead-letter-queue";
private static Logger logger = LoggerFactory.getLogger(DeadLetterQueueConsumer.class);
@RabbitListener(queues = DEAD_LETTER_QUEUE)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
logger.info("接收到死信队列消息:{}", msg);
}
}
使用队列TTL时存在一个问题,就是每增加一个新的时间需求,就需要新增一个队列。下面使用消息TTL实现延迟消费
3. 消息TTL实现延时消费
3.1. 消息TTL配置类
@Configuration
public class TtlMsgConfig {
private static final String TTL_MSG_QUEUE_EXCHANGE = "ttl-msg-queue-exchange";
private static final String DEAD_LETTER_EXCHANGE = "dead-letter-exchange";
private static final String TTL_MSG_QUEUE = "ttl-msg-queue";
private static final String DEAD_LETTER_ROUT_KEY = "dead-letter-routing-key";
private static final String TTL_MSG_QUEUE_ROUT_KEY = "ttl_msg_queue-routing-key";
@Bean(name = "ttlMsgQueueExchange")
public DirectExchange ttlMsgQueueExchange() {
return new DirectExchange(TTL_MSG_QUEUE_EXCHANGE);
}
@Bean(name = "ttlMsgQueue")
public Queue ttlMsgQueue() {
Map<String, Object> map = new HashMap<>(3);
map.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
map.put("x-dead-letter-routing-key", DEAD_LETTER_ROUT_KEY);
return QueueBuilder.durable(TTL_MSG_QUEUE).withArguments(map).build();
}
@Bean
public Binding ttlMsgQueueBinding(@Qualifier("ttlMsgQueue") Queue queue, @Qualifier("ttlMsgQueueExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(TTL_MSG_QUEUE_ROUT_KEY);
}
}
3.2. 消息TTL生成者类
@RestController
@RequestMapping("/ttl-msg")
public class TtlMsgQueueProducerController {
private static final String TTL_MSG_QUEUE_EXCHANGE = "ttl-msg-queue-exchange";
private static final String TTL_MSG_QUEUE_ROUT_KEY = "ttl_msg_queue-routing-key";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}/{ttlTime}")
public void sendMessage(@PathVariable String message, @PathVariable String ttlTime) {
String uuid = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(uuid);
rabbitTemplate.convertAndSend(TTL_MSG_QUEUE_EXCHANGE, TTL_MSG_QUEUE_ROUT_KEY, message, processor -> {
processor.getMessageProperties().setExpiration(ttlTime);
return processor;
}, correlationData);
}
}
使用消息TTL存在另一个问题,即如果有多个消息,第一个消息的延时时长很长,第二个消息的延时时长很短,当第二个消息过期了,却不会优先处理第二个消息,这是因为RabbitMQ只会检查第一个消息是否过期。下面使用延时插件解决这个问题
4. 插件实现延迟消费
4.1. 下载插件
访问官网下载插件
进入下载页面,选择合适的版本
4.2. 安装延时插件
下载插件后上传到rabbitmq插件目录,一般为/usr/lib/rabbitmq/lib/rabbitmq_server-3.9.5/plugins,使用命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange使插件生效
然后重新启动RabbitMQ即可
插件安装成功后,在RabbitMQ管理控制台可以查看到这个插件
4.3. 延迟插件配置类
@Configuration
public class RabbitDelayedConfig {
private static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
private static final String DELAYED_QUEUE_NAME = "delayed.queue";
private static final String DELAYED_ROUT_KEY = "delayed.routingkey";
@Bean(name = "delayedExchange")
public CustomExchange delayedExchange() {
Map<String, Object> map = new HashMap<>(1);
map.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, map);
}
@Bean(name = "delayedQueue")
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUT_KEY).noargs();
}
}
4.4. 延时插件生产者类
@RestController
@RequestMapping("/delayed")
public class DelayedProducerController {
private static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
private static final String DELAYED_ROUT_KEY = "delayed.routingkey";
private static Logger logger = LoggerFactory.getLogger(DelayedProducerController.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}/{delayTime}")
public void sendMessage(@PathVariable String message, @PathVariable Integer delayTime) {
String uuid = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(uuid);
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUT_KEY, message, processor -> {
processor.getMessageProperties().setDelay(delayTime);
return processor;
}, correlationData);
logger.info("发送一条延时:{}毫秒的消息:{}给队列delayed.queue", delayTime, message);
}
}
4.5. 延时插件消费者类
@Component
public class DelayedConsumer {
private static final String DELAYED_QUEUE_NAME = "delayed.queue";
private static Logger logger = LoggerFactory.getLogger(DelayedConsumer.class);
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
logger.info("接收到延时队列消息:{}", msg);
}
}
4.6. 验证延时插件
需要注意的是,如果配置了发送回调ReturnCallback,延迟插件队列会回调该方法,因为发送方没有将消息投递到队列,只是存放在交换机上暂存,等过期时间到了才会发往队列