抽奖项目第三篇
【抽奖项目】|第三篇
前言: 高并发的活动预热肯定不可以在数据库操作,需要redis,特别是这种秒杀活动更是需要注意,所以可以在高并发的前夕先进行活动预热。 上一篇写完了怎么样活动预热,可以看看这篇写怎么样高并发抽奖接口 前端点击活动,返回给后端活动的id 在redis中查询活动的id,判断活动的状态 存在{结束,未开始,正在开始} 不存在{活动不存在} 存在: 获取用户的登录状态,先进行判断的登录处理,登录以后redis分布式锁,并且mq处理发送消息给mysql存储用户参与抽奖的消息 获取当前的用户,可以抽奖多少次,中奖多少次 开始抽奖,通过lua脚本和redis抽奖,返回token,最后返回相对应的信息 未抽中,没有奖品,没有抽奖次数,达到最大抽奖数,奖品已抽完直接返回消息 抽中:返回消息给前端,并且通过mq来处理消息写入mysql数据库 1、为什么要使用定时调度? 一分钟一次查询数据库,防止数据库压力过大 2、为什么要使用lua脚本,而且不是使用java语言来处理? lua的原子性操作: Lua脚本在Redis中是原子执行的:这意味着一旦一个Lua脚本开始执行,它将一直运行直到完成,期间不会被其他命令打断。这对于需要保证一系列操作(例如检查、更新数据等)作为一个不可分割的整体非常有用。如果这些操作使用多个独立的Redis命令来实现,则可能会导致竞态条件(race condition),特别是在高并发环境下。 减少网络往返 提高性能:通过使用Lua脚本,可以将多个命令组合在一起并一次性发送到Redis服务器执行,减少了客户端与服务器之间的多次通信开销。如果使用Java直接调用Redis命令,则可能需要多次网络请求来完成同样的逻辑,增加了延迟和复杂度。 灵活和便携
易于编写和维护的小型任务:对于一些简单的逻辑处理或特定于应用的操作,使用Lua脚本可以更加灵活和便捷。Lua语言设计简洁,语法清晰,适合快速开发小型脚本。 嵌入式脚本支持
Redis原生支持Lua脚本:Redis提供了一种机制,允许用户直接在Redis实例上运行Lua脚本。这使得你可以利用Redis的强大数据结构和操作能力,同时保持代码的紧凑性和高效性。 隔离业务逻辑 分离关注点**:** 通过将特定业务逻辑封装在Lua脚本中,可以在一定程度上分离数据库层面的逻辑与应用程序的其他部分。这种方式有助于简化主应用程序的设计,并且可以更容易地对这部分逻辑进行修改和优化,而无需重新部署整个Java应用程序。 @GetMapping("/go/{gameid}") @ApiOperation(value = “抽奖”) @ApiImplicitParams({ @ApiImplicitParam(name = “gameid”, value = “活动id”, example = “1”, required = true) }) public ApiResult act(@PathVariable int gameid, HttpServletRequest request) { // TODO:任务6.1-抽奖业务-抽奖接口 //1、获取活动的基本信息 CardGame game = (CardGame) redisUtil.get(RedisKeys.INFO + gameid); if (game == null) { return new ApiResult<>(-1, “活动不存在”, null); } // 3、获取当前时间 Date currentDate = new Date(); // 4、判断当前时间是否大于结束时间 if (currentDate.after(game.getEndtime())) { System.out.println(“活动已结束”); return new ApiResult<>(-1, “活动已结束”, null); } // 5、判断当前时间是否小于开始时间 if (currentDate.before(game.getStarttime())) { return new ApiResult<>(-1, “活动未开始”, null); } //2、获取当前登录的用户 HttpSession session = request.getSession(); CardUser user = (CardUser) session.getAttribute(“user”); if (user == null) { return new ApiResult<>(-1, “未登录”, null); } else { if (redisUtil.setNx(RedisKeys.USERGAME + user.getId() + “” + gameid, 1, (game.getEndtime().getTime() - currentDate.getTime()) / 1000)) { CardUserGame userGame = new CardUserGame(); userGame.setUserid(user.getId()); userGame.setGameid(gameid); userGame.setCreatetime(new Date()); rabbitTemplate.convertAndSend(RabbitKeys.EXCHANGE_DIRECT, RabbitKeys.QUEUE_PLAY, JSON.toJSONString(userGame)); } } //获取最大抽奖次数 Integer maxEnter = (Integer) redisUtil.hget(RedisKeys.MAXENTER + gameid, user.getLevel() + “”); maxEnter = maxEnter == null ? 0 : maxEnter; if (maxEnter > 0) { //获取目前的抽奖次数,然后比较 long enter = redisUtil.incr(RedisKeys.USERENTER + gameid + “” + user.getId(), 1); if (enter > maxEnter) { return new ApiResult<>(-1, “您的抽奖的次数已用完”, null); } } //获取最大中奖数 Integer maxCount = (Integer) redisUtil.hget(RedisKeys.MAXGOAL + gameid, user.getLevel() + “”); maxCount = maxCount == null ? 0 : maxCount; Long token; switch (game.getType()) { case 1: token = luaScript.tokenCheck(gameid, user.getId(), maxCount); if (token == 0) { return new ApiResult<>(0, “未中奖”, null); } else if (token == -1) { return new ApiResult<>(-1, “您已达到最大中奖数”, null); } else if (token == -2) { return new ApiResult<>(-1, “奖品已抽光”, 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(-1, “不支持的活动类型”, null); } CardProduct product = (CardProduct) redisUtil.get(RedisKeys.TOKEN + gameid + “” + token); CardUserHit hit = new CardUserHit(); hit.setGameid(gameid); hit.setHittime(currentDate); hit.setProductid(product.getId()); hit.setUserid(user.getId()); rabbitTemplate.convertAndSend(RabbitKeys.EXCHANGE_DIRECT, RabbitKeys.QUEUE_HIT, JSON.toJSONString(hit)); return new ApiResult(1, “恭喜中奖”, product); } lua脚本 – 获取token并判定是否中奖 – 返回值:-1=可用抽奖次数不足,-2=奖品被抽光,0=有令牌但你未中奖,else=中奖,返回了拿到的令牌 redis.log(redis.LOG_NOTICE, “– 开始抽奖操作 :gameId,userId,maxGoal=” .. KEYS[1] .. ‘,’ .. KEYS[2] .. ‘,’ .. KEYS[3]) – 用户已中奖次数 local usergoal = redis.call(‘get’,‘user_hit’ .. KEYS[1] .. ‘’ .. KEYS[2]) – 中奖次数大于最大允许次数,返回-1 if usergoal ~= false and tonumber(KEYS[3]) ~=0 and tonumber(usergoal) >= tonumber(KEYS[3]) then redis.log(redis.LOG_NOTICE, “– 中奖次数超出上限,tonumber(usergoal) > tonumber(KEYS[3]) , return -1”) return -1 end – 从左侧获取一个token local token = redis.call(’lpop’, ‘game_tokens’ .. KEYS[1]) – 当前系统时间 local curtime = redis.call(‘TIME’)[1] redis.log(redis.LOG_NOTICE, “– 当前时间,curtime = " .. curtime) if token ~= false then redis.log(redis.LOG_NOTICE, “– 获取到令牌,token = " .. token) – token是毫秒,并且尾部加了3位随机数,curtime是秒,相差6位 if tonumber(token)/1000 > curtime1000 then redis.log(redis.LOG_NOTICE, “– 令牌无效,tonumber(token)/1000 > curtime1000 , return 0”) redis.call(’lpush’, ‘game_tokens_’ .. KEYS[1], token) return 0 – 否则表示token有效,中奖,用户中奖数+1,返回token令牌 else local hit = redis.call(‘incr’,‘user_hit_’ .. KEYS[1] .. ‘_’ .. KEYS[2]) redis.log(redis.LOG_NOTICE, “– 令牌有效,中奖! return token,userGoal=” .. tonumber(hit)) return tonumber(token) end else – 取不到token,表示抽光了,返回-2 redis.log(redis.LOG_NOTICE, “– 取不到token,奖品已抽光,返回-2”) return -2 end LuaScript @Service public class LuaScript { @Autowired private RedisTemplate redisTemplate; private DefaultRedisScript script; @PostConstruct public void init(){ script = new DefaultRedisScript(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource(new ClassPathResource(“lua/tokenCheck.lua”))); } /*
- 调lua脚本获取token
- gameId: 活动id, userId:当前登录用户的id, maxCount:当前活动允许的最大中奖次数
- */
public Long tokenCheck(int gameId,int userId,int maxCount){
List keys = new ArrayList();
keys.add(String.valueOf(gameId));
keys.add(String.valueOf(userId));
keys.add(String.valueOf(maxCount));
Long result = (Long) redisTemplate.execute(script,keys,0,0);
return result;
}
}
最后压测的成绩
1000个人10000次