【抽奖项目】|第二篇
前言:
高并发的活动预热肯定不可以在数据库操作,需要redis,特别是这种秒杀活动更是需要注意,所以可以在高并发的前夕先进行活动预热。
思路:
1、 通过定时任务调度每分钟查询数据库也没有需要预热的活动
2、采用分布式锁防止任务重复调度
3、查询到预热活动需要信息全部进行redis存储
4、生成令牌桶
细节:生成总奖品个数个令牌
每个令牌生成开始到结束时间的一个随机数
乘上1000,在额外加上一个三位数的随机数 ------防止奖品过多令牌重复
把令牌放入令牌桶
设置令牌和奖品的关系
5、先按照时间大小排序,在压入redis
6、改变预热状态
@Scheduled(cron = "0 * * * * ?")
public void execute() {
//TODO 缓存预热
// 获取当前时间的Calendar实例
Calendar calendar = Calendar.getInstance();
// 清除毫秒部分
calendar.set(Calendar.MILLISECOND, 0);
// 获取不带毫秒的Date对象
Date now = calendar.getTime();
//分布式锁,防止重复启动任务
if (!redisUtil.setNx("game_task_"+now.getTime(),1,60L)){
log.info("task started by another server!");
return;
}
//查询将来1分钟内要开始的活动
QueryWrapper<CardGame> gameQueryWrapper = new QueryWrapper<>();
//开始时间大于当前时间
gameQueryWrapper.gt("starttime",now);
//小于等于(当前时间+1分钟)
gameQueryWrapper.le("starttime",DateUtils.addMinutes(now,1));
List<CardGame> list = gameService.list(gameQueryWrapper);
if(list.size() == 0){
//没有查到要开始的活动
log.info("没有查到要开始的活动");
return;
}
log.info("需要缓存预热的活动个数:{}",list.size());
//有相关活动数据,则将活动数据预热,进redis
list.forEach(game ->{
//活动开始时间
long start = game.getStarttime().getTime();
//活动结束时间
long end = game.getEndtime().getTime();
//计算活动结束时间到现在还有多少秒,作为redis key过期时间
long expire = (end - now.getTime())/1000;
// long expire = -1; //永不过期
//活动持续时间(ms)
long duration = end - start;
Map queryMap = new HashMap();
queryMap.put("gameid",game.getId());
//活动基本信息
game.setStatus(1);
redisUtil.set(RedisKeys.INFO+game.getId(),game,-1);
log.info("活动ID:{},名称:{},开始:{},结束{}", game.getId(),game.getTitle(),game.getStarttime(),game.getEndtime());
//活动奖品信息
List<CardProductDto> products = gameLoadService.getByGameId(game.getId());
Map<Integer,CardProduct> productMap = new HashMap<>(products.size());
products.forEach(p -> {
productMap.put(p.getId(),p);
});
//奖品数量等配置信息
List<CardGameProduct> gameProducts = gameProductService.listByMap(queryMap);
//令牌桶
List<Long> tokenList = new ArrayList();
gameProducts.forEach(cgp ->{
//生成amount个start到end之间的随机时间戳做令牌
for (int i = 0; i < cgp.getAmount(); i++) {
long rnd = start + new Random().nextInt((int)duration);
//为什么乘1000,再额外加一个随机数呢? - 防止时间段奖品多时重复
//记得取令牌判断时间时,除以1000,还原真正的时间戳
long token = rnd * 1000 + new Random().nextInt(999);
//将令牌放入令牌桶
tokenList.add(token);
//token到实际奖品之间建立映射关系
redisUtil.set(RedisKeys.TOKEN + game.getId() +"_"+token,productMap.get(cgp.getProductid()),expire);
}
});
//排序后放入redis队列
Collections.sort(tokenList);
log.info("load tokens:{}",tokenList);
//从右侧压入队列,从左到右,时间戳逐个增大
redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(),tokenList);
redisUtil.expire(RedisKeys.TOKENS + game.getId(),expire);
//奖品策略配置信息
List<CardGameRules> rules = gameRulesService.listByMap(queryMap);
//遍历策略,存入redis hset
rules.forEach(r -> {
redisUtil.hset(RedisKeys.MAXGOAL +game.getId(),r.getUserlevel()+"",r.getGoalTimes());
redisUtil.hset(RedisKeys.MAXENTER +game.getId(),r.getUserlevel()+"",r.getEnterTimes());
redisUtil.hset(RedisKeys.RANDOMRATE +game.getId(),r.getUserlevel()+"",r.getRandomRate());
});
redisUtil.expire(RedisKeys.MAXGOAL +game.getId(),expire);
redisUtil.expire(RedisKeys.MAXENTER +game.getId(),expire);
redisUtil.expire(RedisKeys.RANDOMRATE +game.getId(),expire);
//活动状态变更为已预热,禁止管理后台再随便变动
game.setStatus(1);
gameService.updateById(game);
});
}