springboot企业级抽奖项目业务四 (缓存预热)
缓存预热
为什么要做预热:
当活动真正开始时,需要超高的并发访问活动相关信息 必须把必要的数据提前加载进redis
预热的策略:
在msg中写一个定时任务
每分钟扫描一遍card_game表
把(开始时间 > 当前时间)&& (开始时间 <= 当前时间+1分钟)的活动及相关信息放入redis
缓存预热流程:
代码上需要取出满足条件的活动列表,对每个活动查出相应的奖品放到令牌桶,查出相应的活动策略放到Redis
1、查询1分钟内的活动
2、循环遍历活动列表,挨个处理,假设当前取出的是A
3、查询A相关的奖品列表及数量
4、根据总数量生成奖品相关的令牌桶
5、查询A相关的活动策略:抽奖次数、中奖次数等,放入Redis
缓存体系

1)活动基本信息 k-v,以活动id为key,活动对象为value,永不超时
redisUtil.set(RedisKeys.INFO+game.getId(),game,-1);
2)活动策略信息
使用hset,以活动id为group,用户等级为key,策略值为value
redisUtil.hset(RedisKeys.MAXGOAL + game.getId(),r.getUserlevel()+"",r.getGoalTimes());
redisUtil.hset(RedisKeys.MAXENTER +
game.getId(),r.getUserlevel()+"",r.getEnterTimes());
3)抽奖令牌桶 双端队列,以活动id为key,在活动时间段内,随机生成时间戳做令牌,有多少个奖品就生成多少个令牌。令牌即奖品发放的时间点。从小到大排序后从右侧入队。
redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(),tokenList);
4)奖品映射信息
k-v , 以活动id_令牌为key,奖品信息为value,会员获取到令牌后,如果令牌有效,则用令牌token值,来这里获取 奖品详细信息
redisUtil.set(RedisKeys.TOKEN + game.getId() +"_"+token,cardProduct,expire);
5)令牌设计技巧
假设活动时间间隔太短,奖品数量太多。那么极有可能产生的时间戳发生重复。
解决技巧:额外再附加一个随机因子。将 (时间戳 * 1000 + 3位随机数)作为令牌。抽奖时,将抽中的令牌/1000 ,还原真实的时间戳。
//活动持续时间(ms)
long duration = end - start;
long rnd = start + new Random().nextInt((int)duration);
//为什么乘1000,再额外加一个随机数呢? - 防止时间段奖品多时重复
long token = rnd * 1000 + new Random().nextInt(999);
6)中奖计数 k-v,以活动id_用户id作为key,中奖数为value,利用redis原子性,中奖后incr增加计数。 抽奖次数计数也是同样的道理
redisUtil.incr(RedisKeys.USERHIT+gameid+"_"+user.getId(),1);
7)中奖逻辑判断 : 抽奖时,从令牌桶左侧出队和当前时间比较,如果令牌时间戳小于等于当前时间,令牌有效,表示中奖。大于当前 时间,则令牌无效,将令牌还回,从左侧压入队列。
代码开发
代码在msg项目下的GameTask里,已集成Spring调度

看了下Spring调度 ,@Scheduled内写循环时间,下面的代码会定时执行

commons模块下有个RedisKeys,已经定义了可用的Redis key前缀,可以直接使用
接下来补全GameTask中的函数
先用QueryWrapper取到下一分钟所有的任务
// 获取当前时间
Date now = new Date();// 查询将来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.isEmpty()) {// 没有查到要开始的活动log.info("No upcoming games within the next minute.");
} else {log.info("Found {} upcoming games.", list.size());
}
对于每个任务,获取到要存入Redis中的信息
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;// 创建查询参数的MapMap<String, Object> queryMap = new HashMap<>();queryMap.put("gameid", game.getId());
先将基本信息存入Redis
// 活动基本信息
game.setStatus(1);
redisUtil.set(RedisKeys.INFO + game.getId(), game, -1);
log.info("Loaded game info: {}, {}, {}, {}", game.getId(), game.getTitle(), game.getStarttime(), game.getEndtime());
把奖品放入map
// 活动奖品信息
List<CardProductDto> products = gameLoadService.getByGameId(game.getId());
Map<Integer, CardProduct> productMap = new HashMap<>(products.size());
products.forEach(p -> productMap.put(p.getId(), p));
log.info("Loaded product types: {}", productMap.size());
//奖品数量等配置信息
List<CardGameProduct> gameProducts = gameProductService.listByMap(queryMap);
log.info("load bind product:{}",gameProducts.size());
令牌桶创建,然后token存入Redis
// 令牌桶
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("Loaded tokens: {}", tokenList);// 从右侧压入队列,从左到右,时间戳逐个增大
redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(), tokenList);
redisUtil.expire(RedisKeys.TOKENS + game.getId(), expire);
接着将策略存入Redis
// 奖品策略配置信息
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());log.info("Loaded 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);
写完运行发现每隔一分钟尝试将活动信息写入缓存
然后写了个缓存接口来测试,代码从Redis里取数据即可
@GetMapping("/info/{gameid}")
@ApiOperation(value = "缓存信息")
@ApiImplicitParams({@ApiImplicitParam(name="gameid",value = "活动id",example = "1",required = true)
})
public ApiResult info(@PathVariable int gameid) {Map<String, Object> resMap = new LinkedHashMap<>();Map<String, Object> tokenMap = new LinkedHashMap();Object gameInfo = redisUtil.get(RedisKeys.INFO + gameid);Map<Object, Object> maxGoalMap = redisUtil.hmget(RedisKeys.MAXGOAL + gameid);Map<Object, Object> maxEnterMap = redisUtil.hmget(RedisKeys.MAXENTER + gameid);List<Object> tokenList = redisUtil.lrange(RedisKeys.TOKENS + gameid, 0, -1);resMap.put(RedisKeys.INFO + gameid, gameInfo);resMap.put(RedisKeys.MAXGOAL + gameid, maxGoalMap);resMap.put(RedisKeys.MAXENTER + gameid, maxEnterMap);for (Object item : tokenList) {Object tokenData = redisUtil.get(RedisKeys.TOKEN + gameid + "_" + item.toString());Long key = Long.valueOf(item.toString());Date date = new Date(key / 1000);SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");String formattedDate = dateFormat.format(date);tokenMap.put(formattedDate, tokenData);}resMap.put(RedisKeys.TOKENS + gameid, tokenMap);return new ApiResult(200, "缓存信息", resMap);
}
运行测试
在后台数据库启动一个近期活动:

查看Redis

相关文章:
springboot企业级抽奖项目业务四 (缓存预热)
缓存预热 为什么要做预热: 当活动真正开始时,需要超高的并发访问活动相关信息 必须把必要的数据提前加载进redis 预热的策略: 在msg中写一个定时任务 每分钟扫描一遍card_game表 把(开始时间 > 当前时间)&& (开始时间 < 当前时间1分钟)的活动及相…...
opejdk11 java 启动流程 java main方法怎么被jvm执行
java启动过程 java main方法怎么被jvm执行 java main方法是怎么被jvm调用的 1、jvm main入口 2、执行JLI_Launch方法 3、执行JVMInit方法 4、执行ContinueInNewThread方法 5、执行CallJavaMainInNewThread方法 6、创建线程执行ThreadJavaMain方法 7、执行ThreadJavaMain方法…...
link 样式表是否会阻塞页面内容的展示?取决于浏览器,edge 和 chrome 会,但 firefox 不会。
经过实测: 在 head 中 link 一个 1M 大小的样式表。设置网络下载时间大概为 10 秒。 edge 和 chrome 只有在下载完样式表后,页面上才会出现内容。而 firefox 可以直接先显示内容,然后等待样式表下载完成后再应用样式。 DOMContentLoaded 事…...
uniapp对接极光推送(国内版以及海外版)
勾选push,但不要勾选unipush 国内版 网址:极光推送-快速集成消息推送功能,提升APP运营效率 (jiguang.cn) 进入后台,并选择对应应用开始配置 配置安卓包名 以及ios推送证书,是否将生产证书用于开发环境选择是 ios推送证书…...
智慧城市数字孪生,综合治理一屏统览
现代城市作为一个复杂系统,牵一发而动全身,城市化进程中产生新的矛盾和社会问题都会影响整个城市系统的正常运转。智慧城市是应对这些问题的策略之一。城市工作要树立系统思维,从构成城市诸多要素、结构、功能等方面入手,系统推进…...
在Java中对SQL进行常规操作的通用方法
SQL通用方法 一、常规方法增删改查二、具体优化步骤1.准备工作2.getcon()方法,获取数据库连接对象3.closeAll()方法,关闭所有资源4.通用的增删改方法5.通用的查询方法6.动态查询语句 总结 一、常规方法增删改查 在常规方法中,我们在Java中对…...
JavaSE day16笔记 - string
第十六天课堂笔记 学习任务 Comparable接口★★★★ 接口 : 功能的封装 > 一组操作规范 一个抽象方法 -> 某一个功能的封装多个抽象方法 -> 一组操作规范 接口与抽象类的区别 1本质不同 接口是功能的封装 , 具有什么功能 > 对象能干什么抽象类是事物本质的抽象 &…...
java将文件转成流文件返回给前端
环境:jdk1.8,springboot2.5.3,项目端口号:9100 1.待转换的文件 一、路径 二、文件内容 2.controller中代码 package com.example.pdf.controller;import com.example.pdf.service.GetFileStreamService; import org.springframework.web.b…...
使用Node.js常用命令提高开发效率
Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,广泛用于构建服务器端应用程序和命令行工具。Node.js提供了丰富的命令和工具,可以帮助开发者更高效地开发应用程序。在日常开发中,除了Node.js本身的核心功能外,npm&#x…...
百度资源平台链接提交
百度资源平台是百度搜索引擎提供的一个重要工具,用于帮助网站主将自己的网站链接提交给百度搜索引擎,以便更快地被收录和展示在搜索结果中。以下将就百度资源平台链接提交的概念、操作方法以及其对网站收录和曝光的影响进行探讨: 什么是百度资…...
力扣爆刷第108天之CodeTop100五连刷26-30
力扣爆刷第108天之CodeTop100五连刷26-30 文章目录 力扣爆刷第108天之CodeTop100五连刷26-30一、15. 字符串相加二、300. 最长递增子序列三、42. 接雨水四、43. 重排链表五、142. 环形链表 II 一、15. 字符串相加 题目链接:https://leetcode.cn/problems/add-strin…...
Android裁剪图片为波浪形或者曲线形的ImageView
如果需要做一个自定义的波浪效果的进度条,裁剪图片,对ImageView的图片进行裁剪,比如下面2张图,如何实现? 先看下面的效果,看到其实只需要对第一张高亮的图片进行处理即可,灰色状态的作为背景图。…...
Linux课程____shell脚本应用
:一、认识shell 常用解释器 Bash , ksh , csh 登陆后默认使用shell,一般为/bin/bash,不同的指令,运行的环境也不同 二、 编写简单脚本并使用 # vim /frist.sh //编写脚本文件,简单内容 #!/bin/bash …...
设计模式12--组合模式
定义 案例一 案例二 优缺点...
【微服务】软件架构的演变之路
目录 单体式架构的时代单体式架构(Monolithic)优点缺点适用场景单体式架构面临诸多问题1.宽带提速,网民增多2.Web2.0时代的特点问题描述优化方向 集群优点缺点适用场景搭建集群后面临诸多问题用户请求问题用户的登录信息数据查询 改进后的架构 垂直架构优点缺点 分布…...
安全算法 - 加密算法
加密算法是一种在信息安全领域中广泛应用的算法,能够将数据进行加密转换,以保证数据的保密性和安全性。 它具有保密性、对称加密和非对称加密、密钥管理、数据完整性和认证等重要特点和应用。 加密算法可以分为对称加密和非对称加密两种类型࿱…...
安全算法 - 国密算法
国密算法是中国自主研发的密码算法体系,包括对称加密算法、非对称加密算法和哈希算法。其中,国密算法采用SM4作为对称加密算法,SM2作为非对称加密算法,以及SM3作为哈希算法。国密算法在信息安全领域具有重要意义和广泛应用&#x…...
蓝桥杯2014年第十三届省赛真题-武功秘籍
一、题目 武功秘籍 小明到X山洞探险,捡到一本有破损的武功秘籍(2000多页!当然是伪造的)。他注意到:书的第10页和第11页在同一张纸上,但第11页和第12页不在同一张纸上。 小明只想练习该书的第81页到第92页的…...
Could not initialize class java.awt.Font
项目场景: 项目场景:java项目在web端导出Excel、Word、PDF等文档 问题描述 在Windows系统中开发以及运行文件导出正常,单机部署到Linux中或者使用docker部署后,导出报错。 异常: eleasing transactional SqlSession…...
Mysql or与in的区别
创建一个表格 内涵一千万条数据 这张表中,只有id有建立索引,且其余都没有 测试1:使用or的情况下,根据主键进行查询 可以看到根据主键id进行or查询 花费了30-114毫秒,后面30多毫秒可能是因为Mysql的Buffer Pool缓冲池的…...
ksail:本地Kubernetes开发环境一键搭建与云原生实践
1. 项目概述:当Kubernetes遇上本地开发如果你是一名后端或云原生方向的开发者,大概率经历过这样的场景:为了调试一个微服务,你需要在本地启动一整套依赖——数据库、消息队列、缓存,可能还有另外两三个兄弟服务。你手忙…...
5分钟搞定Mac Boot Camp驱动:告别繁琐手动安装的智能工具
5分钟搞定Mac Boot Camp驱动:告别繁琐手动安装的智能工具 【免费下载链接】brigadier Fetch and install Boot Camp ESDs with ease. 项目地址: https://gitcode.com/gh_mirrors/bri/brigadier 还在为Mac电脑安装Windows驱动而头疼吗?Brigadier是…...
Windows安卓应用安装神器:APK Installer完整使用指南
Windows安卓应用安装神器:APK Installer完整使用指南 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为Windows电脑无法运行安卓应用而烦恼吗ÿ…...
Arduino与MAX4080S联手:打造高精度微安级电流监测方案
1. 为什么需要微安级电流监测? 在开发低功耗设备时,电流监测就像给设备装上了"健康监测仪"。我做过一个智能手环项目,发现待机状态下整机电流只有23微安,用普通万用表根本测不准,数值跳得跟心电图似的。这时…...
Proteus仿真入门:手把手教你用51单片机点亮共阳数码管(附完整代码与电路图)
Proteus仿真入门:51单片机驱动共阳数码管全流程解析 第一次接触单片机仿真时,看着那些闪烁的数码管总觉得神奇又遥远。记得我大三那年,为了完成课程设计,在实验室熬了三个通宵才让数码管显示出正确的数字。今天,我们就…...
2026年AI一键生成歌曲软件精选:音潮 V3.0 零基础闭眼入
2026 年 AI 音乐创作全面大众化,AI 一键生成歌曲软件已经成为日常创作刚需。市面上音潮、Melo、Suno、海绵音乐等AI 音乐生成工具层出不穷,上手难度、成品质感、中文适配度差距明显。经过多轮实测,音潮 V3.0 综合体验一骑绝尘,成为…...
ncmdumpGUI:解锁网易云音乐NCM文件格式的终极解决方案
ncmdumpGUI:解锁网易云音乐NCM文件格式的终极解决方案 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 还在为网易云音乐下载的NCM格式文件无法在其…...
告别手动下载!3步轻松批量获取网易云音乐FLAC无损音乐
告别手动下载!3步轻松批量获取网易云音乐FLAC无损音乐 【免费下载链接】NeteaseCloudMusicFlac 根据网易云音乐的歌单, 下载flac无损音乐到本地.。 项目地址: https://gitcode.com/gh_mirrors/nete/NeteaseCloudMusicFlac 你是不是也遇到过这样的烦恼&#x…...
Nigate:让Mac与Windows硬盘和谐共处的开源桥梁
Nigate:让Mac与Windows硬盘和谐共处的开源桥梁 【免费下载链接】Free-NTFS-for-Mac Nigate: An open-source NTFS utility for Mac. It supports all Mac models (Intel and Apple Silicon), providing full read-write access, mounting, and management for NTFS …...
QQ音乐加密文件解密终极指南:qmcdump工具完整教程
QQ音乐加密文件解密终极指南:qmcdump工具完整教程 【免费下载链接】qmcdump 一个简单的QQ音乐解码(qmcflac/qmc0/qmc3 转 flac/mp3),仅为个人学习参考用。 项目地址: https://gitcode.com/gh_mirrors/qm/qmcdump 你是否曾经…...
