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;

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

扫描电子显微镜如何对焦

扫描电子显微镜&#xff08;SEM&#xff09;的对焦过程是确保获得清晰、高分辨率图像的关键步骤。与传统光学显微镜相比&#xff0c;SEM使用电子束来照射样品&#xff0c;并通过检测样品发射的二次电子或背散射电子来生成图像。因此&#xff0c;对焦方法和原理也有所不同。以下…

模板设计模式经典案例

模板设计模式讲究的是将不变的设置为基类&#xff0c;将变的设置为虚函数来让子类实现。下面就以这样的写下模板设计模式的例子。 例子场景 一个工程步骤分为step1,step2&#xff0c;其中step1由总工程指定&#xff0c;step2由子工程指定&#xff0c;最后由一个函数串起来&am…

python 蓝桥杯之枚举、尺取法

文章目录 幸运数字组合型枚举排列型枚举python 排列函数子集尺取法 幸运数字 对于该题的思路&#xff1a;我们可以采用直接暴力枚举的方法 幸运数字就是 3 i 3^i 3i 5 j 5^j 5j 7 k 7^k 7k 的形式&#xff0c;那么我们就确定一个终极范围&#xff0c;直接用三层循环来遍历即可…

ruoyi-nbcio-plus基于vue3的flowable任务切换的主文件ElementTask.vue的升级修改

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

Linux:Jenkins全自动持续集成持续部署(4)

在上一章部署好了之后&#xff0c;还需要点击一下才能进行部署&#xff0c;本章的效果是&#xff1a;当gitlab上的代码发生了变化后&#xff0c;我们不需要做任何事情不需要去点击构建按钮&#xff0c;Jenkins直接自动检测变化&#xff0c;然后自动去集成部署Linux&#xff1a;…

vue3路由代码示例

路由简单分三步吧 第一定义路由&#xff1a; 包括访问地址&#xff0c;对应组件 第二使用&#xff1a;在index.html中使用它 下面是代码示例&#xff1a; components/Person.vue <template><router-link to"/test/hello">跳转</router-link>&…

RocketMQ学习笔记:分布式事务

这是本人学习的总结&#xff0c;主要学习资料如下 马士兵教育rocketMq官方文档 目录 1、分布式事务的难题2、解决方式2.1、半事务消息和事务回查2.2、代码样例2.2.1、TransactionListener2.2.2、TransactionMQProducer2.2.3、MessageListenerConcurrently2.2.4、流程图 1、分布…

浅谈双亲委派模型

本文浅析了双亲委派的基本概念、实现原理、和自定义类加载器的正确姿势。 对于更细致的加载loading过程、初始化initialization顺序等问题&#xff0c;文中暂不涉及&#xff0c;后面整理笔记时有相应的文章。 JDK版本&#xff1a;oracle java 1.8.0_102 基本概念 定义 双亲委…

智慧城市解决方案大全:标准规范顶层设计指南、整体解决方案、厂商售前宣讲PPT、招投标、智慧城市白皮书等全套680份,一次性打包下载

关键词&#xff1a;智慧城市&#xff0c;智慧城市解决方案&#xff0c;智慧城市发展的前景与趋势&#xff0c;智慧城市概念主力流出&#xff0c;智慧城市项目包括哪些方面&#xff0c;智慧城市项目方案&#xff0c;智慧城市宣传片&#xff0c;智慧城市白皮书&#xff0c;智慧城…

Linux如何将桌面版转为mini版-解决中文字体变为英文字体

中文字体转为英文字体 我们进入Rocky-Linux后&#xff0c;ls或者打开文件夹发现有中文 我们执行命令 sudo localedef -i en_US -f UTF-8 en_US.UTF-8将其转为英文&#xff0c;并且重启机器 此时中文转化为英文 桌面版linux转为MINN版 1. 我们可以卸载桌面版 sudo dnf gr…

c++ 跳转搜索(Jump Search)

与二分搜索一样&#xff0c;跳转搜索是一种针对排序数组的搜索算法。基本思想是通过按固定步骤向前跳跃或跳过某些元素来代替搜索所有元素来检查更少的元素&#xff08;比线性搜索&#xff09;。例如&#xff0c;假设我们有一个大小为 n 的数组 arr[] 和一个大小为 m 的块&…

PEReDi 完全隐私的央行数字货币方案

第一个对完全隐私保护建模的方案&#xff0c;基于账户模型&#xff0c;要求交易双方都在线。 角色分类 中央银行 B B B&#xff1a;负责发行数字货币和货币政策&#xff0c;但不控制用户账户的状态&#xff0c;没有能力对交易的发送者或接收者进行去匿名化或披露与特定交易相…

数据结构-队列-005

1链式队列 运行结果如下&#xff1a; 1.1链式队列结点定义 /*自定义一个数据类型*/ typedef struct student {char name[32];char sex;int age; }DATA_TYPE;/*定义一个链式队列结点*/ typedef struct link_queue_node {DATA_TYPE data;//数据域struct link_queue_node *pne…

SpringBoot和SpringCloud面试题

1、SpringBoot 1.1 和Spring对比 1.2 SpringBoot自动装配 springboot的自动装配实际上就是为了从spring.factories文件中获取到对应的需要进行自动装配的类&#xff0c;并生成相应的Bean对象&#xff0c;然后将它们交给spring容器来帮我们进行管理 原理 SpringBootApplicatio…

BUUCTF-Misc13

[ACTF新生赛2020]outguess1 1.打开附件 2.outguess outguess -k "abc" -r mmm.jpg flag.txt “-k “abc”” 表示使用密码 “abc” 进行解密&#xff1b; “-r” 表示提取信息的操作&#xff1b; “mmm.jpg” 是包含隐藏信息的源图像文件&#xff1b; “flag.txt” …

共用体详解

1 共用体的概念 有时需要使几种不同类型的变量存放到同一段内存单元中。例如,可把一个整型变量、一个字符型变量、一个实型变量放在同一个地址开始的内存单元中(见图11.24)。以上3个变量在内存中占的字节数不同,但都从同一地址开始(图中设地址为1000)存放。也就是使用覆盖技术…

“数据持久化”和“缓存与数据库不一致”到底有什么区别?

之前&#xff0c;我一直把“数据持久化”和“缓存与数据库不一致问题”给搞混了。我当时复习的时候基本上就没有思考&#xff0c;就是纯背诵&#xff0c;数据持久化是什么&#xff0c;数据持久化有两种方式&#xff0c;这两种方式特点是什么&#xff0c;然后巴拉巴拉一堆。缓存…

LC 100.相同的树

100. 相同的树 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a; p [1,2,3], q [1,2,3] 输出&#xff1…

【pytest、playwright】构建POM项目,以及解决登录问题,allure环境问题

目录 前言 1、文件目录 2、安装依赖 3、POM项目实战-案例&#xff1a;打开指定页面 目录结构&#xff1a; pages中的代码&#xff1a; cases中的代码&#xff1a; 4、解决登录问题 问题&#xff1a; 解决方案&#xff1a; 获取登录的用户信息&#xff08;cookie&a…

GTC 2024 火线评论:DPU 重构文件存储访问

编者按&#xff1a;英伟达2024 GTC 大会上周在美国加州召开&#xff0c;星辰天合 CTO 王豪迈在大会现场参与了 GPU 与存储相关的最新技术讨论&#xff0c;继上一篇《GTC 2024 火线评论&#xff1a;GPU 的高效存储利用》之后&#xff0c;这是他发回的第二篇评论文章。 上一篇文章…