【抽奖项目】|第三篇

devtools/2025/3/13 13:29:24/

前言:

        高并发的活动预热肯定不可以在数据库操作,需要redis,特别是这种秒杀活动更是需要注意,所以可以在高并发的前夕先进行活动预热。

        上一篇写完了怎么样活动预热,可以看看这篇写怎么样高并发抽奖接口
【抽奖项目】|第二篇https://blog.csdn.net/EdgeAI/article/details/146214301?spm=1001.2014.3001.5501

        前端点击活动,返回给后端活动的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应用程序。

java">   @GetMapping("/go/{gameid}")@ApiOperation(value = "抽奖")@ApiImplicitParams({@ApiImplicitParam(name = "gameid", value = "活动id", example = "1", required = true)})public ApiResult<Object> 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]) thenredis.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 thenredis.log(redis.LOG_NOTICE, "-- 获取到令牌,token = " .. token)-- token是毫秒,并且尾部加了3位随机数,curtime是秒,相差6位if tonumber(token)/1000 > curtime*1000 thenredis.log(redis.LOG_NOTICE, "-- 令牌无效,tonumber(token)/1000 > curtime*1000 , return 0")redis.call('lpush', 'game_tokens_' .. KEYS[1], token)return 0-- 否则表示token有效,中奖,用户中奖数+1,返回token令牌elselocal 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,表示抽光了,返回-2redis.log(redis.LOG_NOTICE, "-- 取不到token,奖品已抽光,返回-2")return -2
end
LuaScript
java">@Service
public class LuaScript {@Autowiredprivate RedisTemplate redisTemplate;private DefaultRedisScript<Long> script;@PostConstructpublic void init(){script = new DefaultRedisScript<Long>();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<String> 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次


http://www.ppmy.cn/devtools/166781.html

相关文章

pytorch心德

以下是一些关于写Pytorch项目心得的思路呀&#xff1a; 项目整体流程方面 环境搭建与数据准备 描述在安装Pytorch及相关依赖库时遇到的问题及解决办法&#xff0c;比如CUDA版本与Pytorch版本的匹配问题等。 谈一谈数据收集、清洗、标注等过程的难度和重要性&#xff0c;以及在处…

JAVA实现好看的俄罗斯方块小游戏(附源码)

文章目录 一、设计来源俄罗斯方块小游戏讲解1.1 主界面1.2 游戏中界面1.3 游戏结束界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载更多优质源码分享 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/146156297 JAVA…

Javascript 2016-2024 新特性讲解

JavaScript 作为世界上最流行的编程语言之一&#xff0c;一直在不断发展和进化。从 2016 年的 ES7 到 2024 年的 ES15&#xff0c;每年都会有新的语言特性和功能被添加进来。这些更新不仅提升了语言的表达能力&#xff0c;也为开发者提供了更多便利的工具和更优雅的解决方案。 …

算法题(94):除2!

审题&#xff1a; 本题需要我们对n个数据中的偶数数据进行不大于k次除2操作&#xff0c;使得n个数据的总和最小 思路&#xff1a; 方法一&#xff1a;贪心与优先级队列&#xff08;大堆&#xff09; 贪心策略&#xff1a;我们每次都对目前最大的偶数进行除2的操作 策略证明&…

AI科技公司招聘一位后端开发工程师

招聘岗位&#xff1a;后端开发工程师&#xff08;兼运维&#xff09; 公司名称&#xff1a;深圳市格子科技有限公司 公司介绍&#xff1a;深圳市格子科技有限公司作为AI应用创新先锋&#xff0c;构建起以AI工具研发为核心、短剧平台运营为延伸的多业务发展模式。公司自主研发A…

linux在 Ubuntu 系统中设置服务器时间

在 Ubuntu 系统中设置服务器时间通常涉及以下步骤&#xff0c;涵盖自动同步和手动配置两种方式。以下是详细操作指南&#xff1a; 一、检查当前时间状态 timedatectl status输出示例&#xff1a;Local time: Wed 2023-10-18 15:30:00 UTC Universal time: Wed 2023-10-18 15:3…

51单片机Proteus仿真速成教程——P1-软件与配置+Proteus绘制51单片机最小系统+新建程序模版

前言&#xff1a;本文主要围绕 51 单片机最小系统的绘制及程序模板创建展开。首先介绍了使用 Proteus 绘制 51 单片机最小系统的详细步骤&#xff0c;包括软件安装获取途径、工程创建、器件添加&#xff08;如单片机 AT89C51、晶振、电容、电阻、按键等&#xff09;、外围电路&…

【蓝桥杯速成】| 1.暴力解题

1高频考点与暴力解题_哔哩哔哩_bilibili 感谢up主分享&#xff0c;以下内容是学习笔记&#xff0c;以c为主&#xff0c;部分python 题目一&#xff1a;维纳的年龄 题目内容 美国数学家维纳(N.Wiener)智力早熟&#xff0c; 11岁就上了大学。他曾在1935~1936年应邀来中国清华大…