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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pytorch反向传播算法

目录 1. 链式法则复习2. 多输出感知机3. 多层感知机4. 多层感知机梯度推导5. 反向传播的总结 1. 链式法则复习 2. 多输出感知机 3. 多层感知机 如图&#xff1a; 4. 多层感知机梯度推导 简化式子把( O k O_k Ok​ - t k t_k tk​) O k O_k Ok​(1 - O k O_k Ok​)起个别名…

国产数据库中统计信息自动更新机制

数据库中统计信息描述的数据库中表和索引的大小数以及数据分布状况&#xff0c;统计信息的准确性对优化器选择执行计划时具有重要的参考意义。本文简要整理了下传统数据库和国产数据库中统计信息的自动更新机制&#xff0c;以加深了解。 1、数据库统计信息介绍 优化器是数据库…

数据泵impdp导入时间特别久及导入中断后继续导入案例分享

欢迎关注“数据库运维之道”公众号&#xff0c;一起学习数据库技术! 本期将为大家分享“数据泵impdp导入时间特别久及导入中断后继续导入”的性能优化案例。 关键词&#xff1a;Streams AQ: enqueue blocked on low memory、Impdp Hang、Datapump Export/Import、STREAMS_POOL_…

QT数据类型和容器用法

Qt库提供了基于通用模板的容器类, 这些类可用于存储指定类型的数据项&#xff0c;Qt中这些容器类的设计比STL容器更轻&#xff0c;更安全且更易于使用。容器类也都是隐式共的&#xff0c;它们是可重入的&#xff0c;并且已针对速度/低内存消耗和最小的内联代码扩展进行了优化&a…

数据平台“国产替代”掣肘在迁移?奇点云的工业制造实践解读

系列导读 如《“数据要素”三年行动计划&#xff08;2024—2026年&#xff09;》指出&#xff0c;工业制造是“数据要素”的关键领域之一。如何发挥海量数据资源、丰富应用场景等多重优势&#xff0c;以数据流引领技术流、资金流、人才流、物资流&#xff0c;对于制造企业而言是…

【Functional Affordances】如何确认可抓取的区域?(前传)

文章目录 1. 【Meta AI】Emerging Properties in Self-Supervised Vision Transformers2. 【Meta AI】DINOv2: Learning Robust Visual Features without Supervision3. 【NeurIPS 2023】Diffusion Hyperfeatures: Searching Through Time and Space for Semantic Corresponden…

机器学习——神经网络简单了解

一、神经网络基本概念 神经网络可以分为生物神经网络和人工神经网络 (1)生物神经网络,指的是生物脑内的神经元、突触等构成的神经网络&#xff0c;可以使生物体产生意识&#xff0c;并协助生物体思考、行动和管理各机体活动。 (2)人工神经网络,是目前热门的深度学习的研究…

rust中常用cfg属性和cfg!宏的使用说明,实现不同系统的条件编译

cfg有两种使用方式&#xff0c;一种是属性&#xff1a; #[cfg()]&#xff0c;一种是宏&#xff1a;cfg! &#xff0c;这两个都是非常常用的功能。 #[cfg()]是 Rust 中的一个属性 用于根据配置条件来选择性地包含或排除代码。cfg 是 "configuration" 的缩写&#xf…

jupyter lab使用虚拟环境

python -m ipykernel install --name 虚拟环境名 --display-name 虚拟环境名然后再启动jupyter lab就行了

项目设计方案:市交通视频监控平台项目设计方案(二)

目录 1 前言 1.1 目的 1.2 适用范围 1.3 术语表 2 现状分析 2.1 业务现状 2.2 组织机构现状 2.3 存在的问题 2.4 项目成果预期 3 系统建设原则 4 项目需求 4.1 项目需求 4.1.1 业务需求主要分为三部分&#xff1a; 4.1.2 技术需求主要分为四部分&#xff1a; 4.…

SpringBoot可以同时处理多少请求

SpringBoot默认的内嵌容器是Tomcat&#xff0c;即看Tomcat可以处理多少请求 默认配置 server:tomcat:threads:min-spare: 10 # 最小工作线程数max: 200 # 最大线程数max-connections: 8192 # 接受和处理的最大连接数&#xff0c;超过8192的请求就会被放入到等待队列中ac…

52个AIGC视频生成算法模型介绍

基于Diffusion模型的AIGC生成算法日益火热&#xff0c;其中文生图&#xff0c;图生图等图像生成技术普遍成熟&#xff0c;很多算法从业者开始从事视频生成算法的研究和开发&#xff0c;原因是视频生成领域相对空白。 AIGC视频算法发展现状 从2023年开始&#xff0c;AIGC视频的新…