springboot企业级抽奖项目业务四 (缓存预热)

news2024/11/25 5:00:18

缓存预热

为什么要做预热:

当活动真正开始时,需要超高的并发访问活动相关信息 必须把必要的数据提前加载进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;

    // 创建查询参数的Map
    Map<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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1557724.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(八)Gateway服务网关

Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;该项目是基于 Spring 5.0&#xff0c;Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关&#xff0c;它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。…

day01-SpringCloud01(Eureka、Ribbon、Nacos)

视频地址: SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务 学习资料地址: 百度网盘 提取码&#xff1a;1234 1. 认识微服务 1.1.单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#x…

WebCopilot:一款功能强大的子域名枚举和安全漏洞扫描工具

关于WebCopilot WebCopilot是一款功能强大的子域名枚举和安全漏洞扫描工具&#xff0c;该工具能够枚举目标域名下的子域名&#xff0c;并使用不同的开源工具检测目标存在的安全漏洞。 工具运行机制 WebCopilot首先会使用assetsfinder、submaster、subfinder、accumt、finddom…

【2023】kafka在linux和docker安装(kafka-1)

目录&#x1f4bb; 一、linux安装kafka1. 安装jdk2. 上传解压到/usr/local目录下3、使用kafka 二、docker安装kafka1. 下载2. 安装zookeeper3. 安装kafka 一、linux安装kafka 环境主机 mac m2、虚拟机Ubuntu22.04.4 1. 安装jdk yum install -y java-1.8.0-openjdk.x86_64下载k…

Redis分布式锁红锁

Redisson实现分布式锁 lock()上锁解析&#xff1a; 1&#xff0c;hexist判断redis是否有这个锁 2&#xff0c;hset设置锁&#xff0c;hash类型&#xff0c;key为锁名字&#xff0c;value是一对kv&#xff0c;k是当前redisson1的id,v为计数器&#xff0c;表示当前锁持有次数&am…

【详细讲解语言模型的原理、实战与评估】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

【苹果MAC】苹果电脑 LOGI罗技鼠标设置左右切换全屏页面快捷键

首先键盘设置->键盘快捷键 调度中心 设置 f1 f2 为移动一个空间&#xff08;就可以快捷移动了&#xff09; 想要鼠标直接控制&#xff0c;就需要下载官方驱动&#xff0c;来设置按键快捷键&#xff0c;触发 F1 F2 安装 LOGI OPTIONS Logi Options 是一款功能强大且便于使用…

HarmonyOS 应用开发之Stage模型绑定FA模型ServiceAbility

本小节介绍Stage模型的两种应用组件如何绑定FA模型ServiceAbility组件。 UIAbility关联访问ServiceAbility UIAbility关联访问ServiceAbility和UIAbility关联访问ServiceExtensionAbility的方式完全相同。 import common from ohos.app.ability.common; import hilog from o…

LabelConvert: 目标检测和图像分割数据集格式转换工具

LabelConvert LabelConvert是一个目标检测和图像分割的数据集格式转换工具&#xff0c;支持labelme、labelImg与YOLO、VOC和COCO 数据集格式之间的相互转换。 支持的转换格式 安装 pip install label_convert具体使用方法 由于文章篇幅所限&#xff0c;请移步LabelConvert官…

Linux初学(十一)中间件

一、web服务 1.1 中间件简介 中间件其实就是一类软件&#xff0c;中间件的作用是让用户可以看到一个网页 总结&#xff1a;客户端可以向服务端发送请求&#xff0c;服务器端会通过中间件程序来接收请求&#xff0c;然后处理请求&#xff0c;最后将处理结果返回给客户端 1.2 中…

【MySql数据库】MySQL5.7在navicat中建立连接报错1045及重装MySQL过程中3306端口号被占用释放的过程

文章目录 一、报错1、软件中报错2、navicat中报错3、数据库密码是正确的4、卸载数据库5、重装数据库发现3306端口被占用 二、释放3306端口1、找到3306端口对应的PID值2、释放3306端口号3、释放端口后&#xff0c;重装数据库 一、报错 1、软件中报错 2、navicat中报错 在navic…

cocos2.x => node 属性修改

简介 与节点属性相关的几个核心变量_trs、_matrix、_worldMatrix、_localMatDirty、_worldMatDirty。 _trs&#xff1a;存储节点的position、rotation、scale _matrix&#xff1a;存储节点的缩放、位移、旋转三者合一的变化矩陈&#xff08;仿射矩陈&#xff09; _worldMat…

linux基础命令篇:Linux基础命令讲解——文件浏览(cat、less、head、tail和grep)

Linux基础命令讲解——文件浏览&#xff08;cat、less、head、tail和grep&#xff09; 本文详细介绍Linux中的cat、less、head、tail和grep命令&#xff0c;这些命令在日常工作中非常实用&#xff0c;以下是关于这些命令的详细介绍&#xff1a; 1. cat命令&#xff1a;用于查看…

简明Pytorch分布式训练 — DistributedDataParallel 实践

上一次的Pytorch单机多卡训练主要介绍了Pytorch里分布式训练的基本原理&#xff0c;DP和DDP的大致过程&#xff0c;以及二者的区别&#xff0c;并分别写了一个小样作为参考。小样毕竟还是忽略了很多细节和工程实践时的一些处理方式的。实践出真知&#xff0c;今天&#xff08;简…

每日面经分享(http、python)

1. http和HTTPS的区别&#xff0c;http的响应流程 a. 区别&#xff1a; i. 安全性&#xff1a;HTTP是明文传输数据&#xff0c;不提供加密保护&#xff0c;因此数据容易被窃听和篡改。而HTTPS使用SSL/TLS协议对数据进行加密和身份验证&#xff0c;提供更高的安全性。 ii. 端口…

新网站秒收录技术,新网站百度收录时间

在建立新网站后&#xff0c;让它尽快被搜索引擎收录是网站主最为关注的事情之一。百度作为中国最大的搜索引擎&#xff0c;网站被其快速收录对于增加曝光和流量至关重要。本文将介绍一些新网站秒收录技术&#xff0c;以及一般情况下新网站被百度收录需要的时间。 新网站秒收录技…

百度资源平台链接提交

百度资源平台是百度搜索引擎提供的一个重要工具&#xff0c;用于帮助网站主将自己的网站链接提交给百度搜索引擎&#xff0c;以便更快地被收录和展示在搜索结果中。以下将就百度资源平台链接提交的概念、操作方法以及其对网站收录和曝光的影响进行探讨&#xff1a; 什么是百度资…

docker-compose安装jenkins

1、环境准备&#xff1a;准备安装好docker的服务器一台 2、在服务器上创建一个目录用于安装Jenkins mkdir jenkins3、下载好要挂载的&#xff1a;maven、jkd&#xff1b;并将下载好的tar.gz包上传至服务器待安装目录中并解压 tar -xzvf tar -xzvf apache-maven-3.9.6-bin.tar…

最短路-Floyd Dijkstrea

Floyd算法 一种求解“多源最短路”问题的算法 在Floyd算法中&#xff0c;图一般用邻接矩阵存储&#xff0c;边权可正可负&#xff08;但不允许负环&#xff09;&#xff0c;利用动态规划的思想&#xff0c;逐步求解出任意两点之间的最短距离 int d[N][N],初始为无穷 d[i][j…

生信数据分析——GO+KEGG富集分析

生信数据分析——GOKEGG富集分析 目录 生信数据分析——GOKEGG富集分析1. 富集分析基础知识2. GO富集分析&#xff08;Rstudio&#xff09;3. KEGG富集分析&#xff08;Rstudio&#xff09; 1. 富集分析基础知识 1.1 为什么要做功能富集分析&#xff1f; 转录组学数据得到的基…