前言
在我们的业务中,经常会使用到延迟队列,今天主要来讲解下目前主流使用的几种实现延迟效果的实现方式。目前只写了下面4种,后期补充RocketMQ,kafka
主要方式
1.通过redisson的set存储原理实现延迟队列
2.通过RabbitMQ的ttl队列+死信路由策略
3.使用RabbitMQ的延迟插件
4.使用redis的key的过期通知策略,(由于key的数量过多后,就会出现延迟,无法在正确的key过期时间通知到服务端,不考虑使用)
Redisson实现方式
依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.7</version>
</dependency>
实现
package com.gery.redis.common.configuration;
import com.gery.redis.listener.RedissonListener;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* @Description: RedissonQueueInit
* @Author: gery
* @Date: 2022/11/10 18:40
*/
@Slf4j
@Component
public class RedissonQueueListeners implements CommandLineRunner {
@Resource
private RedissonClient client;
@Resource
private List<RedissonListener> redissonListenerList;
/***
* @Description: startThread
*
* @Param: [queueName, redissonListener]
* @return: void
* @Author: gery
* @Date: 2022/12/30
*/
private <T> void startThread(String queueName, RedissonListener redissonListener) {
RBlockingQueue<T> blockingFairQueue = client.getBlockingQueue(queueName);
//处理 可能由于服务器宕机导致消息未被消费
client.getDelayedQueue(blockingFairQueue);
//新起线程监听队列
Thread thread = new Thread(() -> {
while (true) {
String message = "";
try {
message = (String) blockingFairQueue.take();
log.info("监听队列线程,监听名称:{},内容:{}", queueName, message);
redissonListener.doService(message);
} catch (Exception e) {
log.info("监听线程错误,", e);
}
}
});
thread.setName(queueName);
thread.start();
}
/***
* @Description: run
*
* @Param: [args]
* @return: void
* @Author: gery
* @Date: 2022/12/30
*/
@Override
public void run(String... args) throws Exception {
redissonListenerList.forEach(redissionListener -> startThread(redissionListener.getClass().getName(), redissionListener));
}
}
原理
Redisson延迟队列使用三个队列:【消息延时队列】、【消息顺序队列】、【消息目标队列】。
- 将元素及延时信息入队,之后定时任务将到期的元素转移到阻塞队列
- 使用HashedWheelTimer做定时,定时到期之后从zset中取头部100个到期元素,所以定时和转移到阻塞队列是解耦的,无论是哪个task触发的pushTask,最终都是先取zset的头部先到期的元素
- 元素数据都是存在redis服务端的,客户端只是执行HashedWheelTimer任务,所以单个客户端挂了不影响服务端数据,做到分布式的高可用。
ttl队列+死信
条件
- 当消息被消费者拒绝接收,并且不会被其他消费者消费,消息就会到达死信队列
- 消息被设置了ttl时间,并且这个时间已经到期了,消息就会到达死信队列
- 队列已经达到了最大限度,排在前面的消息会被丢弃或到私信队列
原理
坑点
- 绑定私信交换机时,一定要将x-dead-letter的参数值设置正确,不然过期消息无法正确路由
- 时序问题,由于RabbitMQ是一个FIFO有序队列,遵循先进先出原则,且队列只会判断第一个消息是否过期,不会对队列上的每一个消息都做过期判断,所以会出现长时间过期的消息阻塞短时间过期的情况,这就导致不能在一个队列中设置不同过期时间
使用RabbitMQ插件实现
说明
当前办法需要安装延时消息插件(rabbitmq-delayed-message-exchange)我们可以声明 x-delayed-message 类型的 Exchange,消息发送时指定消息头 x-delay 以毫秒为单位将消息进行延迟投递。
原理
- 当消息到达声明x-delayed-message的交换机时,他的消息在发布之后不会立即进入队列,而是将消息保存至Mnesia种
- 这个插件将会尝试确认消息是否过期,首先要确保消息的延迟范围是 Delay > 0, Delay =< ?ERL_MAX_T
- 如果消息过期通过 x-delayed-type 类型标记的交换机投递至目标队列,整个消息的投递过程也就完成了。
坑点
- 不要下载过高版本的RabbitMQ和插件,下载一个稳定的版本即可
- 开启插件后一定要重启RabbitMQ,否则不生效,如果重启也没生效,你可以尝试下kill掉MQ的进程,然后再启动
- 开启消息确认机制后,生产者生产的消息都会进入returncallback回调中。⭐⭐⭐
- 一旦开启了消息确认机制,延迟消息每次都会先进入returncallback回调,然后才会投递成功,这里涉及到延迟插件的原理,首先是交换机设置了延迟投递delayed:true,延迟消息实际上是挂载到交换机上,不会马上就通过路由投递出去,而returncallback回调打印出来的返回信息:replyText=NO_ROUTE,很明显了吧,说是没有路由,所以消息确认失败了,因为延迟插件没有马上通过路由投递。
- 有个mandatory参数,mandatory为true表示开启强制投递,为false表示不强制,而且这个值可以为null, 如果我们的yml文件中开启了消息确认机制:publisher-returns: true,则每次一定会走returncallback回调,所以我们要么不开启消息确认机制,要么就把mandatory设置为false,这样延迟插件的消息就不会每次走一遍回调了
总结
第一次写文章,上述方法都在代码中测试过, 文章凡是有不足的地方麻烦指下