抽奖优化设计3
前面两篇文章谈到了抽奖的优化设计。抽奖的主要性能问题在于锁的粒度过大,每个租户一个锁,一个租户只允许一个请求进入抽奖流程。 抽奖过程的优化思路是降低锁的粒度,将锁粒度减小为奖品级别,但是不论是使用redis分布式锁,或者mysql本身的行锁,最终中奖都要进行数据奖品数量的-1,数据库往往会成为瓶颈。经过思考后,决定使用redis做数据库存储,异步线程将redis数据同步至数据库,并基于redis+lua的无锁实现抽奖算法。
Redis存储奖品信息
- 将抽奖活动中奖品的三个关键信息:权重,中奖次数,剩余奖品数存入redis,抽奖活动过程中,奖品数量的更改不立即保存至数据库,先在redis中进行奖品数量的加减。异步线程定时将redis中的奖品数量,权重,中奖次数等信息同步至数据库。存储结构:

- 将存放奖品信息的redis,独立一个redis实列,使用aof+appendfsync everysec持久化机制,一定程度上防止数据丢失,应用额外使用日志文件保存中奖结果,当redis意外crash,从aof文件恢复redis后,通过校对日志文件,数据库,redis值确定redis值是否错,有则修改redis值。
Redis+Lua脚本实现抽奖算法
熟悉redis的同学都知道,redis+lua可以实现CAS等功能,单线程的redis在执行lua脚本时,并不会执行其他操作,保证lua脚本中多步操作的原子性,保证线程安全。
实现
从prizeWeighHash中取出所有的奖品权重,构造权重区间,生成随机数,根据随机数和权重区间得出prizeId,调用lua脚本将奖品数量减一,中奖次数加一。
流程图:

更改中奖数量的Lua脚本:
local count=redis.call("HINCRBY",KEYS[1],KEYS[4],-1)
if count<=0 then
redis.call("HDEL",KEYS[1],KEYS[4])
redis.call("HDEL",KEYS[2],KEYS[4])
end
if count>=0 then
redis.call("HINCRBY",KEYS[3],KEYS[4],1)
end
return count
-
Lua脚本将奖品的剩余次数-1,当奖品数量为0,或者对应的hash key不存在,返回count为-1。这时需要将奖品对应的weight hash item和remainCount hash item删除。
-
如果count大于等于0,用户成功中奖,将奖品中奖次数加1。 由于redis在执行lua脚本的过程中,不会执行其他操作。所以对奖品数量的操作是单线程的,并不会有线程安全问题。
-
Lua脚本会存在返回-1的情况,当多个线程同时抽中最后一个奖品时,第一个执行lua脚本的请求会将prizeId所对应的remainCount删除,这时后续的线程执行redis.call("HINCRBY",KEYS[1],KEYS[4],-1),结果将是-1,这是应用程序判断调用lua脚本的结果小于,说明该次抽奖用户没有抽中,返回token给客户端,客户端再发起重试。
当所有奖品被抽光后,prizeRemainCount hash item的长度将为0,以此作为奖品是否被抽光的判断逻辑。
管理后台更新奖品信息
抽奖另一个需求点:可以在管理后台修改奖品数量,修改数量的前置条件是:奖品总数大于中奖次数。
在抽奖过程中,判断奖品总数大于中奖次数,而后更新奖品总数,但这时还是会有抽奖的请求进来,一样会有线程并发的问题。想到的两种解决方式:
- 先停止抽奖活动,再修改奖品数量?但是停止抽奖活动后,并不确定所有的抽奖请求是否已经被处理完毕?
- 管理后台修改奖品也是使用redis+lua脚本,判断totalCount> hitcount,则更新redis 的remain count hash item,并同时更新数据库。
更改奖品数量的Lua脚本:
local hitCount=redis.call("HGET",KEYS[1],KEYS[4])
local remainCount=KEYS[5]-hitCount
if remainCount<0 then
return -1
end
if(remainCount==0) then
redis.call("HDEL",KEYS[2],KEYS[4])
redis.call("HDEL",KEYS[3],KEYS[4])
return 0
end
if (remainCount>0) then
redis.call("HSET",KEYS[2],KEYS[4],remainCount)
redis.call("HSET",KEYS[3],KEYS[4],KEYS[6])
return remainCount
end
如果更新缓存成功,但是更新数据库失败的话,采用监控报警,人工干预的方式介入处理。
利用redis+lua的原子性,将抽奖功能重构无锁实现,并通过引入token重试机制,在并发出现奖品被抽光是立即返回;而不是基于租户级别的分布式悲观锁,造成tomcat大量线程被block住,拖慢整个应用程序。
posted on 2016-04-27 22:21 raymond_kop 阅读(249) 评论(0) 收藏 举报
浙公网安备 33010602011771号