核心编码分为两个部分:活动信息预热和抽奖
定时器从数据库取出活动种子,判断时间
@Scheduled(cron = "0 * * * * ?")
public void execute() {
// 当前时间
Date now = new Date();
// 查询将来1分钟内要开始的活动
CardGameExample example = new CardGameExample();
CardGameExample.Criteria criteria = example.createCriteria();
// 开始时间大于当前时间
criteria.andStarttimeGreaterThan(now);
// 小于等于(当前时间+1分钟)
criteria.andStarttimeLessThanOrEqualTo(DateUtils.addMinutes(now,1));
List<CardGame> list = gameMapper.selectByExample(example);
if(list.size() == 0){
// 没有查到要开始的活动
log.info("game list scan : size = 0");
return;
}
log.info("game list scan : size = {}",list.size());
}
如果存在有相关活动种子,则将活动数据预热,放进缓存
list.forEach(game ->{
//活动开始时间
long start = game.getStarttime().getTime();
//活动结束时间
long end = game.getEndtime().getTime();
//计算活动结束时间到现在还有多少秒,作为redis key过期时间
long expire = (end - now.getTime())/1000;
//活动持续时间(ms)
long duration = end - start;
//活动基本信息
game.setStatus(1);
redisUtil.set(RedisKeys.INFO+game.getId(),game,-1);
log.info("load game info:{},{},{},{}",
game.getId(),game.getTitle(),game.getStarttime(),game.getEndtime());
}
获取活动奖品信息和奖品数量等配置信息,并生成令牌桶,进行奖品信息预热
//活动奖品信息
List<CardProductDto> products = gameLoadMapper.getByGameId(game.getId());
Map<Integer,CardProduct> productMap = new HashMap<>(products.size());
products.forEach(p -> productMap.put(p.getId(),p));
log.info("load product type:{}",productMap.size());
//奖品数量等配置信息
CardGameProductExample productExample = new CardGameProductExample();
productExample.createCriteria().andGameidEqualTo(game.getId());
List<CardGameProduct> gameProducts = gameProductMapper.selectByExample(productExample);
log.info("load bind product:{}",gameProducts.size());
//令牌桶
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);
//以令牌做key,对应的商品为value,创建redis缓存
log.info("token -> game : {} -> {}",token/1000 ,productMap.get(cgp.getProductid()).getName());
//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);
获取奖品策略信息,进行策略预热
CardGameRulesExample rulesExample = new CardGameRulesExample();
rulesExample.createCriteria().andGameidEqualTo(game.getId());
List<CardGameRules> rules = gameRulesMapper.selectByExample(rulesExample);
//遍历策略,存入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());
log.info("load rules:level={},enter={},goal={},rate={}",
r.getUserlevel(),r.getEnterTimes(),r.getGoalTimes(),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);
gameMapper.updateByPrimaryKey(game);
获取活动信息,进行活动和用户信息的基础校验
Date now = new Date();
//获取活动基本信息
CardGame game = (CardGame) redisUtil.get(RedisKeys.INFO+gameid);
//判断活动是否开始
//如果活动信息还没加载进redis,无效
//如果活动已经加载,预热完成,但是开始时间 > 当前时间,也无效
if (game == null || game.getStarttime().after(now)){
return new ApiResult(-1,"活动未开始",null);
}
//判断活动是否已结束
if (now.after(game.getEndtime())){
return new ApiResult(-1,"活动已结束",null);
}
//获取当前用户
HttpSession session = request.getSession();
CardUser user = (CardUser) redisUtil.get(RedisKeys.SESSIONID+session.getId());
if (user == null){
return new ApiResult(-1,"未登陆",null);
}else{
//第一次抽奖,发送消息队列,用于记录参与的活动(redis分布式锁)
if (!redisUtil.hasKey(RedisKeys.USERGAME+user.getId()+"_"+gameid)){
redisUtil.set(RedisKeys.USERGAME+user.getId()+"_"+gameid,1,(game.getEndtime().getTime() - now.getTime())/1000);
//持久化抽奖记录,扔给消息队列处理
CardUserGame userGame = new CardUserGame();
userGame.setUserid(user.getId());
userGame.setGameid(gameid);
userGame.setCreatetime(new Date());
rabbitTemplate.convertAndSend(RabbitKeys.QUEUE_PLAY,userGame);
}
}
//用户可抽奖次数
Integer enter = (Integer) redisUtil.get(RedisKeys.USERENTER+gameid+"_"+user.getId());
if (enter == null){
enter = 0;
redisUtil.set(RedisKeys.USERENTER+gameid+"_"+user.getId(),enter,(game.getEndtime().getTime() - now.getTime())/1000);
}
//根据会员等级,获取本活动允许的最大抽奖次数
Integer maxenter = (Integer) redisUtil.hget(RedisKeys.MAXENTER+gameid,user.getLevel()+"");
//如果没设置,默认为0,即:不限制次数
maxenter = maxenter==null ? 0 : maxenter;
//次数对比
if (maxenter > 0 && enter >= maxenter){
//如果达到最大次数,不允许抽奖
return new ApiResult(-1,"您的抽奖次数已用完",null);
}else{
redisUtil.incr(RedisKeys.USERENTER+gameid+"_"+user.getId(),1);
}
//用户已中奖次数
Integer count = (Integer) redisUtil.get(RedisKeys.USERHIT+gameid+"_"+user.getId());
if (count == null){
count = 0;
redisUtil.set(RedisKeys.USERHIT+gameid+"_"+user.getId(),count,(game.getEndtime().getTime() - now.getTime())/1000);
}
//根据会员等级,获取本活动允许的最大中奖数
Integer maxcount = (Integer) redisUtil.hget(RedisKeys.MAXGOAL+gameid,user.getLevel()+"");
//如果没设置,默认为0,即:不限制次数
maxcount = maxcount==null ? 0 : maxcount;
//次数对比
if (maxcount > 0 && count >= maxcount){
//如果达到最大次数,不允许抽奖
return new ApiResult(-1,"您已达到最大中奖数",null);
}
以上校验全部过关,进入下一步:拿令牌。拿到合法令牌就中奖。
Long token;
switch (game.getType()) {
//时间随机
case 1:
//随即类比较麻烦,按设计时序图走
//java调redis,有原子性问题!
token = (Long) redisUtil.leftPop(RedisKeys.TOKENS+gameid);
if (token == null){
//令牌已用光,说明奖品抽光了
return new ApiResult(-1,"奖品已抽光",null);
}
//判断令牌时间戳大小,即是否中奖
//这里记住,取出的令牌要除以1000,参考job项目,令牌生成部分
if (now.getTime() < token/1000){
//当前时间小于令牌时间戳,说明奖品未到发放时间点,放回令牌,返回未中奖
redisUtil.leftPush(RedisKeys.TOKENS+gameid,token);
return new ApiResult(0,"未中奖",null);
}
break;
case 2:
//瞬间秒杀类简单,直接获取令牌,有就中,没有就说明抢光了
token = (Long) redisUtil.leftPop(RedisKeys.TOKENS+gameid);
if (token == null){
//令牌已用光,说明奖品抽光了
return new ApiResult(-1,"奖品已抽光",null);
}
break;
case 3:
//幸运转盘类,先给用户随机剔除,再获取令牌,有就中,没有就说明抢光了
//一般这种情况会设置足够的商品,卡在随机上
Integer randomRate = (Integer) redisUtil.hget(RedisKeys.RANDOMRATE+gameid,user.getLevel()+"");
if (randomRate == null){
randomRate = 100;
}
//注意这里的概率设计思路:
//每次请求取一个0-100之间的随机数,如果这个数没有落在范围内,直接返回未中奖
if( new Random().nextInt(100) > randomRate ){
return new ApiResult(0,"未中奖",null);
}
token = (Long) redisUtil.leftPop(RedisKeys.TOKENS+gameid);
if (token == null){
//令牌已用光,说明奖品抽光了
return new ApiResult(-1,"奖品已抽光",null);
}
break;
default:
return new ApiResult(0,"不支持的活动类型",null);
}//end switch
拿到合法令牌说明已经中奖,剩下的就是中奖通知,和中奖信息持久化
//以上逻辑走完,拿到了合法的token,说明很幸运,中奖了!
//抽中的奖品:
CardProduct product = (CardProduct) redisUtil.get(RedisKeys.TOKEN + gameid +"_"+token);
//中奖次数加1
redisUtil.incr(RedisKeys.USERHIT+gameid+"_"+user.getId(),1);
//投放消息给队列,中奖后的耗时业务,交给消息模块处理
CardUserHit hit = new CardUserHit();
hit.setGameid(gameid);
hit.setHittime(now);
hit.setProductid(product.getId());
hit.setUserid(user.getId());
rabbitTemplate.convertAndSend(RabbitKeys.EXCHANGE_DIRECT,RabbitKeys.QUEUE_HIT, hit);
//返回给前台中奖信息
return new ApiResult(1,"恭喜中奖",product);
阅读量:2094
点赞量:0
收藏量:0