红包雨架构设计---3、核心编码-灵析社区

提笔写架构

核心编码

核心编码分为两个部分:活动信息预热和抽奖

活动信息预热

取出活动种子

定时器从数据库取出活动种子,判断时间

    @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