话很少说,直接上需求描述:redis
最近须要上一期活动,这个活动是以转盘抽奖为形式的抽奖活动,要求每一个用户用积分进行抽奖,且中奖率为100%即不可出现不中任何奖品的状况,以后,又加了一个要求,即不能实行纯随机的抽取,若是如此会产生一个极端状况,若是开始的时候活动极其火爆因为随机的不可控性头一天用户便将全部优质奖品所有抽走,那么后来的用户将只会抽到保底奖品。数据库
那么奖品就须要按时间分布在从活动开始到结束的时间段,其次须要作的是,在某些特殊的时间段,咱们但愿多投放一些奖品给用户抽到。安全
需求分析:数据结构
那么开奖策略能够为为每一个奖品设置开奖时间,只有在开奖后来抽奖才能抽到该奖品,不然视为未中奖发保底奖品,咱们只须要拿当前时间与最接近奖品开奖时间对比便可。并发
由上需求,那么就须要一个容器来存放这些奖品,对这个容器的要求:分布式
1. 它能够以时间轴为维度取出奖品;高并发
2. 它能够以时间轴为维度放入奖品;性能
3.它能够以时间轴为维度将奖品排序;spa
同时,后台应该有地方配置每一个小时应投放的奖品数量,同时为保证配置数据能及时生效,应当是每小时前去向奖品池投放下一个小时的奖品;debug
以下图所示,每一个奖品都有对应开奖时间,奖品1只有10000毫秒以后的请求才能够抽到,且只有奖品1抽走以后才能够抽奖品2;
抽奖步骤:
性能安全考虑:
显然,抽奖是容易引起并发问题的场景,高并发状况每每会带来两个问题
1. 超发问题,例如将10个奖品发给了11我的,用锁可解决;
2.数据库等基础组件负载太高致使宕机,以数据库为例,若是每一个用户每抽走一个奖品都去链接数据库更新库存,数据库颇有可能承受不住(数据库能承受的qps远不如redis);
方案:
使用redis的zset数据结构,这里简单说明下zset,它是一个基于跳表实现的有序集合,尤为适合排序场景比较多的场景,是一个典型的用空间换取时间的数据结构。这里咱们用开奖时间戳做为score,保证其按照时间排序,存入的时候能够直接将奖品ID与时间戳存入其中便可。
同时设置定时任务,每一个小时去拿下一个小时的所需的奖品,随机将其散列在下一个小时的各个时间上,并在此时就将各个奖品库存扣除。
ok,需求完美解决,锁的问题直接上代码,锁就是保证zset的排序操做与移除操做是原子操做,不然便会出现超发,使用了redis的setNx作分布式锁。
/** * 抽奖 * * @param turnTableNum 转盘编号 * @return 奖品ID */ public long getLotteryResult(long userId, int turnTableNum, Map<Long, ActivityTurntableGoodsConfig> goodsConfigMap) { Set<String> prizeSet = null; String prizeResStr = null; try { if (RedisUtils .lock(RedisKey.TURNTABLE_PRIZE_QUEUE_LOCK, String.valueOf(turnTableNum))) { Set prizeSet = RedisClusterAccessor .zrangeByScore(RedisKey.TURNTABLE_PRIZE_QUEUE, String.valueOf(turnTableNum), 0, System.currentTimeMillis(), 0,1); if (null != prizeResStr) { //在奖池中移除奖品 log.debug("{} remove prize {} {}", XGameContextHolder.get(), turnTableNum, prizeResStr); RedisClusterAccessor .zrem(RedisKey.TURNTABLE_PRIZE_QUEUE, String.valueOf(turnTableNum), prizeResStr); } } } catch (Exception e) { throw e; } finally { RedisUtils.unlock(RedisKey.TURNTABLE_PRIZE_QUEUE_LOCK, String.valueOf(turnTableNum)); } if (null == prizeResStr) { return -1; } return CommonUtil.safeParseLong(prizeResStr.split("_")[0]); }