Redission延迟队列解析

redis 应该怎么实现延时队列

正常使用 redis 可以通过 zset 去实现延迟队列,将 key 设置为延时对象字符串,value 设置为到期的时间戳 通过多个线程不断的去循环判断是否存在到期的任务,如果有就拿出来消费。

伪代码如下

 

java

代码解读

复制代码

while (!Thread.interrupted()) { // 根据分值取出 zset 中的内容,从 0到System.currentTimeMillis(),limit 0,1(只取一个值) Set<String> values = redisUtil.zRangeBySource(key, 0, System.currentTimeMillis(), 0, 1); if (values.isEmpty()) { // 没有到期的数据 1s 以后再重试 Thread.sleep(1000); continue; } String value = values.get(0); // 当一个任务到期时,有多个线程同时拿到了该任务,通过 zrem 来判断到底是哪一个线程执行 if (redisUtil.zrem(key, value) > 0) { handleMsg(value) } }

但上面的代码会有一个问题:多个线程同时获取的values,但是只有一个线程能zrem成功。会增加zRangeBySource的消耗。

这个问题,可以通过 lua 去解决,让zRangeBySourcezrem同时执行。

Redission 是怎么实现延时队列的

下面进入正题,说说 redission 是怎么实现延时队列的,对比 redis 正常的实现有什么好处?

一段常用的使用延迟队列的代码:

 

java

代码解读

复制代码

// 初始化延迟队列 public void queueInit() { blockingQueue = redissonClient.getBlockingQueue(RedisConstant.MEMBER_CARD_ORDER_BLOCKING_QUEUE_NAME); delayedQueue = redissonClient.getDelayedQueue(blockingQueue); startDelayQueueConsumer(); log.info("MemberOrderCreateCmdExe queueInit success"); } // 从队列中阻塞的拿任务 while (true) { try { DelayedCloseOrderBlockQueueBO queueBO = blockingQueue.take(); if (!ObjectUtils.isEmpty(queueBO)) { log.info("监听到延迟关单消息:{}", JsonUtil.of(queueBO)); } } catch (Exception e) { log.error("[MemberOrderCreateCmdExe]监听延迟关单消息异常: {}", e.getMessage()); } } // 向延迟队列中添加任务 delayedQueue.offer(queueBO, 10, TimeUnit.MINUTES);

向延迟队列中添加任务

 

ini

代码解读

复制代码

redissonClient.getBlockingQueue(RedisConstant.MEMBER_CARD_ORDER_BLOCKING_QUEUE_NAME); delayedQueue = redissonClient.getDelayedQueue(blockingQueue); delayedQueue.offer(queueBO, 10, TimeUnit.MINUTES);

追踪源码最终调用

RedissonDelayedQueue类的 offer 方法

offerAsync 是异步调用,外层包裹 get 方法,用redisson 自己的异步执行器同步的获取结果。

要了解这段代码先来看看 timeoutSetName, queueName, channelName 这三个 redis key 分别代表什么

 这里给出四个队列key的命名,后面的内容都以这四个名称命名这四个队列

  1. getRawName():阻塞队列
  2. timeoutSetName:过期时间队列
  3. queueName:有序延迟消息队列
  4. channelName:延迟队列频道

主要内容如下:

  1. 计算出 timeout : 现在的时间戳 + 延迟时间 = 延迟任务的到期时间

  2. 获取一个随机值

  3. 执行一段 lua 脚本

    • 使用 zadd 命令将 argv1(过期时间),value(根据encode(e)计算来 也就是咱们向延迟队列中添加的任务数据) 添加到 key2(过期时间队列) 这个有序列表中,有序列表的 value 是任务的到期时间(默认从小到大进行排序),过期时间近的排在 zset 的上面
    • 用 rpush 命令将 value 再添加到 key3(有序延迟消息队列) 列表中,这里的 key3 只是列表,排序规则就是先 offer 的就排前面
    • 用zrange 取出 key2(过期时间队列)有序列表中第一个元素,如果和本次插入的元素相同那么就publish 发布本次延迟任务过期时间的订阅消息到 key4(延迟队列频道)中

从延迟队列中拿任务

 

java

代码解读

复制代码

blockingQueue = redissonClient.getBlockingQueue(RedisConstant.MEMBER_CARD_ORDER_BLOCKING_QUEUE_NAME); delayedQueue = redissonClient.getDelayedQueue(blockingQueue); DelayedCloseOrderBlockQueueBO queueBO = blockingQueue.take();

  1. 先初始化延迟队列
  2. take 方法底层调用blpop 方法从blockingQueue中阻塞的拿元素
 

scss

代码解读

复制代码

@Override public RFuture<V> takeAsync() { return commandExecutor.writeAsync(getName(), codec, RedisCommands.BLPOP_VALUE, getName(), 0); }

取元素比较简单,只是阻塞的获取,但是会有两个问题

  1. 在添加延迟任务的时候,并没有操作blockingQueue(也就是 offer 对应 lua 脚本中的 key1),那为什么取元素会去那取
  2. 我们取任务的时候没有用到delayedQueue,为什么还需要初始化。

这两个问题在后面初始化延迟队列都会解答

初始化延迟队列

 

ini

代码解读

复制代码

blockingQueue = redissonClient.getBlockingQueue(RedisConstant.MEMBER_CARD_ORDER_BLOCKING_QUEUE_NAME); delayedQueue = redissonClient.getDelayedQueue(blockingQueue);

初始化延迟队列是最复杂的。 从redisson.getDelayedQueue(queue1);跟源码,找到

整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

ini

代码解读

复制代码

protected RedissonDelayedQueue(Codec codec, CommandAsyncExecutor commandExecutor, String name) { super(codec, commandExecutor, name); channelName = prefixName("redisson_delay_queue_channel", getRawName()); queueName = prefixName("redisson_delay_queue", getRawName()); timeoutSetName = prefixName("redisson_delay_queue_timeout", getRawName()); QueueTransferTask task = new QueueTransferTask(commandExecutor.getServiceManager()) { @Override protected RFuture<Long> pushTaskAsync() { return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); " + "if #expiredValues > 0 then " + "for i, v in ipairs(expiredValues) do " + "local randomId, value = struct.unpack('Bc0Lc0', v);" + "redis.call('rpush', KEYS[1], value);" + "redis.call('lrem', KEYS[3], 1, v);" + "end; " + "redis.call('zrem', KEYS[2], unpack(expiredValues));" + "end; " // get startTime from scheduler queue head task + "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); " + "if v[1] ~= nil then " + "return v[2]; " + "end " + "return nil;", Arrays.asList(getRawName(), timeoutSetName, queueName), System.currentTimeMillis(), 100); } @Override protected RTopic getTopic() { return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName); } }; commandExecutor.getServiceManager().getQueueTransferService().schedule(queueName, task); }

该方法大体逻辑是构造出了QueueTransferTask使用QueueTransferService执行该任务

继续跟源码:

 进入 start 方法

收到订阅回调以后会调用 pushTask()方法,注意这里其实就是订阅了,所以会执行回调

先调用了 pushTaskAsync方法 这个方法是主要的逻辑实现:

再调用scheduleTask方法

总结上面的内容到流程图中:

还剩下一个问题为什么我们取元素的时候没有用到delayedQueue,还需要初始化?

从上面的源码中能看到初始化了delayedQueue就可以获取队列中已经到期的 100 个任务放到阻塞队列中,假如生产者生产了消息以后没有来的及把消息放到阻塞队列里去就挂了,这时候客户端去初始化了delayedQueue就能消费到之前的消息。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值