前言
本质上,这个功能是依赖redisson的RDelayedQueue做的一个特殊实现,目的是为了是为了在消息发送后,将其暂时存储在队列中,然后在一定时间后再将其投递给消费者。这种机制通常用于需要在一定时间后执行某些操作的场景,例如延迟任务、定时提醒等。通过延迟队列,可以实现延迟消息的可靠投递,确保消息在指定时间后被消费者接收。
总体逻辑
先说明一下总逻辑
- redissonClient.getDelayedQueue这一步执行之后,会创建和blockingQueueName【最终消费队列】绑定的三个redis集合,
- topic数据类型:channelName = prefixName("redisson_delay_queue_channel", getName());
- 临时队列数据类型:queueName = prefixName("redisson_delay_queue", getName());
- zset数据类型:timeoutSetName = prefixName("redisson_delay_queue_timeout", getName());
然后添加一个task到QueueTransferService的taskmap中,代码:queueTransferService.schedule(queueName, task);
- 下次再执行getDelayedQueue添加task时,会判断taskMap中是否存在有这个blockingQueueName的task,如果有,则继续使用,usage加一;这里有个问题,如果usage一直增加,可能会溢出;
- 任务执行时,会开启两个listener一直监听消费topic的数据,一旦有任务到了执行时间,会将zset里的待执行数据转移到阻塞队列;RedissonDelayedQueue第58行,pushTaskAsync,从zset中读取前100条数据【zrangebyscore】,放到最终消费队列中
- destroy时,task的usage减一,如果减到零,则停止消费上面的topic;
写入逻辑
- 先将数据写到有序集合;
- 数据写到临时队列;
- 取出有序集合第一个元素,如果是当前写入的元素,则将到期时间戳发布到topic;
public RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) {
// 延迟时间
long delayInMs = timeUnit.toMillis(delay);
// 任务到期时间
long timeout = System.currentTimeMillis() + delayInMs;
byte[] random = getServiceManager().generateIdArray(8);
return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_VOID,
"local value = struct.pack('Bc0Lc0', string.len(ARGV[2]), ARGV[2], string.len(ARGV[3]), ARGV[3]);"
+ "redis.call('zadd', KEYS[2], ARGV[1], value);"
+ "redis.call('rpush', KEYS[3], value);"
// if new object added to queue head when publish its startTime
// to all scheduler workers
+ "local v = redis.call('zrange', KEYS[2], 0, 0); "
+ "if v[1] == value then "
+ "redis.call('publish', KEYS[4], ARGV[1]); "
+ "end;",
Arrays.asList(getRawName(), timeoutSetName, queueName, channelName),
timeout, random, encode(e));
}
删除逻辑
- 遍历临时队列;
- 如果当前遍历出来的元素与需要删除的数据相同,则从有序集合和临时队列中删除
- 返回删除数量;
protected RFuture<Boolean> removeAsync(Object o, int count) {
return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_BOOLEAN,
"local s = redis.call('llen', KEYS[1]);" +
"for i = 0, s-1, 1 do "
+ "local v = redis.call('lindex', KEYS[1], i);"
+ "local randomId, value = struct.unpack('Bc0Lc0', v);"
+ "if ARGV[1] == value then "
+ "redis.call('zrem', KEYS[2], v);"
+ "redis.call('lrem', KEYS[1], 1, v);"
+ "return 1;"
+ "end; "
+ "end;" +
"return 0;",
Arrays.<Object>asList(queueName, timeoutSetName), encode(o));
}
定时任务逻辑
topic消费者拿到的数据类型为每个延时数据的到期时间戳,根据拿到的时间戳来生成一个定时任务;在任务中,会执行以下逻辑;
- 取出score小于当前时间戳的前100条记录,然后从有序集合中删除这些数据;
- 从临时队列中删除;
- 将数据写到最终消费的队列中;
- 这100条数据处理完成之后,再取有序队列的第一条数据,并返回时间戳;
- 根据Job返回的时间戳,再生成一个Job;如果时间戳为空,则生成一个延迟5秒的Job;
我的看法
总的来说,这个方案在使用上比较顺手,但是设计过于复杂,对于新手来说难以理解。一般情况下,可以用redis的ScoreSet来自己实现一个延迟队列,大概思路:
- 到期时间的time作为score,放入到scoreSet里;
- 定时读取第一个元素,如果time小于当前时间,则表示已经到期,需要取出来执行;
- 如果第一个元素都没有到期,则后续所有任务都不会到期;