如何用redis实现延时队列

一.概述

在日常开发中,有许许多多的延时处理场景,比如订单超时支付,准点推送抢购通知等等。那么如何实现这种场景呢?
常见的几种方案有:

  1. quartz定时器方案,
  2. 应用内部延时队列
  3. Rabbitmq或kafka的延时队列

这几种方案都有对应的优缺点,比如定时器无法精准扫描,内部延时队列在应用宕机时会丢失,RabbitMq难以支持任意时间段的延时处理等等,本文主要是为了探讨一种新的方案,重点就不放在优缺点比较上 了

二.基本实现

redis提供了多种数据结构,使得我们在一些方案制定上增加了很多便利性,下面我们利用redis的Zset(有序集合)和Set(集合)结构来实现一种某一种业务场景延时队列的实现.

假设伪场景如下:
   我们现在有这么一个需求,从上游接到运输包裹的时候会绑定一个车牌号,现在要做一个需求,接到的运输包裹按照配送区域数量满足十个合并绑定到一个车牌上,当同配送区域数量不满足条件并且等待时间超过四分钟时则直接绑定到对应车牌并通知到对应的分拣人员。

我们如何用redis的数据结构来实现这样一个延时队列呢?

   在这里,我们使用redis的set和zset两种结构来实现这个需求,通过set结构来存放每个区域的包裹明细,zset结构来存放接到第一个同区域包裹的时间戳。

伪代码如下:

 		//存入代码
		if   box == area:
			redis.sadd('set_'+area,box)
			if  None == redis.zscore('zset',area):
				redis.zadd('zset_','推送的时间戳',area)
		if	redis.scard('set_'+area)>=10:
			//执行绑定
			redis.smembers('set_'+area)
			redis.del('set_'+area)
			redis.zrem('zset_',area)
	  //获取满足要求的包裹
		areas = redis.zrangebyscore('zset_',0,'当前时间-需延迟时间的时间戳')
		for area in areas:
			//执行绑定
			boxs=redis.smembers('set_'+area)
			redis.del('set_'+area)
			redis.zrem('zset_',area)

上面这段代码主要是分为两个步骤,第一在接到第一个包裹的时候按照区域埋入当前时间的时间戳,第二步是通过zrangebyscore这个命令来扫描已经满足时间已经达到约定的区域信息,通过这样一种方式,我们简易的延时队列就算是实现了。

三.进阶实现

上面的代码真的没有问题么?
  答案当然是有问题的,实际上,在生产上,往往都是集群部署,上面的操作会出现并发问题, 即缓存的更新与删除不是原子性的。那么我们如何解决并发的问题呢?

redis支持了lua脚本, 保证了在执行一个lua脚本期间,其他任何脚本或者命令都无法执行,这也算是变相的实现了原子性,当然我们不要在脚本中执行过长开销的程序,否则会验证影响其它请求的执行.基于这种前提,如果我们把上面的代码按照lua脚本的方式来重写一遍是不是就能解决多并发下的依赖缓存的原子性呢。
下面我使用lua脚本来完成一个真正的延迟队列.

	KEYS[1]: 区域包裹信息key,
	KEYS[2]:区域时间戳的集合,
	ARGV[1]: 包裹信息
	AGRV[2]:当前时间的时间戳
	AGRV[3]:区域信息
	//之前存放的代码:
	if redis.call('zscore',KEYS[2],ARGV[3]) then
	else
		redis.call('zadd',KEYS[2],ARGV[2],ARGV[3])
	end
	redis.call('sadd',KEYS[1],ARGV[1])
	if redis.call('scard',KEYS[1])>=4 then
		local ll= redis.call('smembers',KEYS[1])
		redis.call('del',KEYS[1])
		redis.call('zrem',KEYS[2],ARGV[3])
		return ll
	end
	return nil
	
	
------------------我是脚本分割线----------------------------------------------------	


KEYS[1]: 区域包裹信息key,
KEYS[2]:区域时间戳的集合,
AGRV[1]:0,
AGRV[2]:当前时间-延迟时间的时间戳
// 之前获取的代码
local time=redis.call('zrangebyscore',KEYS[2],ARGV[1],ARGV[2])
local sch={}
if next(time)~=nil then
  for key,value in pairs(time) do
	local tempkey=KEYS[1]..value
	local templist=redis.call('SMEMBERS',tempkey)
	sch[key]=templist
	redis.pcall('zrem',KEYS[2],value)
	redis.pcall('del',tempkey)
  end
  return sch
else
  return nil
end

上面简单的两个脚本,基本上就实现了redis的延时队列,对比之前一个命令一个命令的获取,是不是简单了很多,不但解决了并发的问题,同时也降低了连接数的消耗。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Redis可以通过两种方式实现延时队列: 1. 使用ZSET实现延时队列 在ZSET中,每一个元素都有一个score值,代表了元素的权重。我们可以把元素的score值设置成到期时间,然后使用Redis的ZREVRANGEBYSCORE命令获取到期时间在当前时间之前的元素,这些元素就是需要被执行的任务。 具体实现流程如下: 1. 将任务添加到延时队列中,设置到期时间为任务的执行时间,score为到期时间的时间戳。 2. 定期轮询延时队列,获取到期时间在当前时间之前的任务,将这些任务从延时队列中移除,并执行相应的操作。 代码示例: ``` from redis import StrictRedis import time redis = StrictRedis(host='localhost', port=6379, db=0) def add_task(task_id, execute_time): redis.zadd('delay_queue', {task_id: execute_time}) def handle_task(): while True: # 获取当前时间戳 current_time = time.time() # 获取到期时间在当前时间之前的任务 tasks = redis.zrangebyscore('delay_queue', 0, current_time) if not tasks: time.sleep(1) continue # 处理任务 for task_id in tasks: # 执行相应的操作 print('Handle task:', task_id) # 从延时队列中移除任务 redis.zrem('delay_queue', task_id) if __name__ == '__main__': # 添加任务 add_task('task1', time.time() + 10) # 处理任务 handle_task() ``` 2. 使用LIST和BLPOP实现延时队列 在LIST中,每一个元素都代表了一个任务。我们可以使用Redis的LPUSH命令将任务添加到LIST中,然后使用Redis的BLPOP命令阻塞获取LIST的最后一个元素,当获取到的元素的score值小于当前时间时,执行相应的操作。 具体实现流程如下: 1. 将任务添加到延时队列中,设置到期时间为任务的执行时间,score为到期时间的时间戳。 2. 定期轮询延时队列,使用BLPOP命令获取LIST的最后一个元素,并判断是否需要执行相应的操作。 代码示例: ``` from redis import StrictRedis import time import threading redis = StrictRedis(host='localhost', port=6379, db=0) def add_task(task_id, execute_time): # 将任务添加到延时队列中,score为到期时间的时间戳 redis.zadd('delay_queue', {task_id: execute_time}) def handle_task(): while True: # 获取当前时间戳 current_time = time.time() # 获取到期时间在当前时间之前的任务 tasks = redis.zrangebyscore('delay_queue', 0, current_time) if not tasks: time.sleep(1) continue # 处理任务 for task_id in tasks: # 执行相应的操作 print('Handle task:', task_id) # 从延时队列中移除任务 redis.zrem('delay_queue', task_id) def push_task(): # 添加任务 add_task('task1', time.time() + 10) add_task('task2', time.time() + 20) add_task('task3', time.time() + 30) # 使用BLPOP命令获取LIST的最后一个元素 while True: value = redis.blpop('task_list', timeout=1) if not value: continue # 判断是否需要执行相应的操作 task_id, score = value if float(score) <= time.time(): print('Handle task:', task_id) if __name__ == '__main__': # 启动处理任务的线程 handle_thread = threading.Thread(target=handle_task) handle_thread.start() # 启动添加任务的线程 push_thread = threading.Thread(target=push_task) push_thread.start() ``` 以上两种方式均可以实现延时队列,具体选择哪种方式取决于实际需求和场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值