携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
简介
延迟队列存储的对象是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
常用场景
- 订单在十分钟之内未支付则自动取消
- 用户注册成功后,如果三天内没有登录则进行短信提醒
- 用户发起退款后,如果三天内没有得到处理则通知相关运营人员
- 用户下单外卖以后,距离超时时间还有 10 分钟时提醒外卖小哥即将超时
- .....
RabbitMq实现延迟队列方案
RabbitMq实现延迟队列的方案有如下两种:
- 第一种:DLX+TTL(Time To Live); 设置TTL分为两种:在队列属性中设置TTL,或者消息属性中设置TTL
- 第二种:使用延时队列插件
DLX+TTL实现延时队列
DLX全称(Dead-Letter-Exchange)称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。
TTL全称(Time To Live)称为过期时间,TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
实现原理
延迟队列为常见的使用模式,如下图所示,生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,由于这些消息设置过期时间,等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。
具体实现
死信队列配置
@Configuration
public class DeadLetterConfig
{
/**
* 普通队列
*/
public static final String TTL_EXCHANGE_NAME = "ttl_exchange";
public static final String TTL_ROUTING_KEY = "ttl_routing_key";
public static final String TTL_QUEUE_NAME = "ttl_queue";
/**
* 死信队列名称
*/
public static final String DLX_QUEUE_NAME = "dlx_queue";
/**
* 死信交换机
*/
public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
/**
* 死信绑定Key
*/
public static final String DLX_ROUTING_KEY = "dlx_routing_key";
/**
* 死信交换机
* @return
*/
@Bean
DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE_NAME, true, false);
}
/**
* 死信队列
* @return
*/
@Bean
Queue dlxQueue() {
return new Queue(DLX_QUEUE_NAME, true, false, false);
}
/**
* 绑定死信队列和死信交换机
* @return
*/
@Bean
Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(DLX_ROUTING_KEY);
}
/**
* 普通交换机
* @return
*/
@Bean
DirectExchange ttlExchange() {
return new DirectExchange(TTL_EXCHANGE_NAME, true, false);
}
/**
* 普通消息队列
* @return
*/
@Bean
Queue ttlQueue()
{
Map<String, Object> args = new HashMap<>(3);
//设置死信交换机
args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
//设置死信 routing_key
args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
//设置消息过期时间 为10秒
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(TTL_QUEUE_NAME).withArguments(args).build();
}
/**
* 普通队列通过js_routing_key绑定到死信交换机
* @return
*/
@Bean
Binding jsBinding() {
return BindingBuilder.bind(ttlQueue())
.to(ttlExchange())
.with(TTL_ROUTING_KEY);
}
}
消息发送者
@Component
public class DeadLetterProduce
{
private Logger logger =LoggerFactory.getLogger(DelayedMqProduce.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(String exchange, String routeKey, String message)
{
String msg = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))+",message:"+message;
logger.info("send dead letter time:{}",msg);
rabbitTemplate.convertAndSend(exchange, routeKey, msg);
}
}
说明:消息和队列都设置了过期时间,则获取最小的时间为准。
消息接收者
@Component
@RabbitListener(queues = "dlx_queue")
public class DeadLetterMqConsumer
{
private static final Logger logger = LoggerFactory.getLogger(DeadLetterMqConsumer.class);
@RabbitHandler
public void receive(String message)
{
logger.info("发送消息的时间:{}",message);
logger.info("消费者收到延迟消息时间:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
说明:注意监听的队列名称为死信队列名称。
测试
@RequestMapping("/sendDeadLetterMessage")
public String sendDeadLetterMessage()
{
deadLetterProduce.sendMessage("ttl_exchange", "ttl_routing_key", "this is dead letter message");
return "success";
}
小结
由于消息采用先进先出的原则,采用此种方案,由于MQ只会检查第一个消息是否过期,如果第一个消息延迟很长时间,会导致第二个消息不会被优先执行,所以需要采用RabbitMq的延迟消息插件来解决此问题。
延时队列插件
Windows安装Rabbitmq安装延时插件
下载延时插件
说明:注意插件的版本与Rabbitmq的版本对应。
将延时插件拷贝到Rabbitmq的插件目录
安装延时插件
进入Rabbitmq的目录的sbin目录,执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
查看插件是否安装成功
登录Rabbitmq后台管理,查询Exchange的类型出现x-delayed-message类型说明安装成功。
实现延迟队列
配置延迟消息
@Configuration
public class DelayedConfig
{
private final String QUEUE_NAME="delayed_queue";
private final String EXCHANGE_NAME="delay_exchange";
@Bean
public Queue delayedQueue()
{
return new Queue(QUEUE_NAME,true);
}
@Bean
CustomExchange customExchange() {
Map<String, Object> args = new HashMap<>();
/
args.put("x-delayed-type", "direct");
//参数二为类型:必须是x-delayed-message
return new CustomExchange(EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
@Bean
Binding binding(Queue queue, CustomExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(QUEUE_NAME).noargs();
}
}
消息发送者
@Component
public class DelayedMqProduce
{
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(String exchangeName,String queueName,String message,long delayTime)
{
rabbitTemplate.convertAndSend(exchangeName,queueName,message,new MessagePostProcessor()
{
@Override
public Message postProcessMessage(Message message) throws AmqpException
{
message.getMessageProperties().setHeader("x-delay",delayTime);
return message;
}
});
}
}
消息接收者
@Component
@RabbitListener(queues = "delayed_queue")
public class DelayedMqConsumer
{
private static final Logger logger = LoggerFactory.getLogger(DelayedMqConsumer.class);
@RabbitHandler
public void receive(String message)
{
logger.info("receive delayedmessage content:{}",message);
}
}
测试结果
2022-08-14 22:53:29.061 [http-nio-9090-exec-4] INFO [] c.s.f.r.produce.DelayedMqProduce - 当 前 时 间 : Sat Aug 13 22:53:29 CST 2022, 发送延迟 3000毫秒的信息给队列 delayed.queue:(Body:'this is delayed message' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=23, deliveryMode=PERSISTENT, priority=0, deliveryTag=0])
2022-08-14 22:53:32.070 [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] INFO [] c.s.f.r.consumer.DelayedMqConsumer - receive delayedmessage content:this is delayed message
总结
本文讲解了Rabbitmq实现延迟队列的两种方案,个人建议采用插件的方式来实现延迟队列,如有疑问,请随时反馈。