在日常开发中,有许许多多的延时处理场景,比如订单超时支付,准点推送抢购通知等等。那么如何实现这种场景呢?
常见的几种方案有:
这几种方案都有对应的优缺点,比如定时器无法精准扫描,内部延时队列在应用宕机时会丢失,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的延时队列,对比之前一个命令一个命令的获取,是不是简单了很多,不但解决了并发的问题,同时也降低了连接数的消耗。