记录自己写项目的第三天,springbot+redis+rabbitma高并发项目
今天实现了缓存预热这个接口任务,文章末端附完整代码
缓存预热的目的就是在活动开始之前,把数据提前存入到redis 中,避免活动开始时,大量数据请求打到数据库
一:定义一个定时任务
每分钟对活动的开始情况进行检测一次,如果有活动开始,就通过下面的操作将他们存入到redis中
二:数据库查询
首先设计咱们查询的时间范围,未来一分钟开始的活动(当前时间 ≤ 活动开始时间 ≤ 未来 1 分钟)
Date now = new Date();
now = DateUtils.truncate(now, Calendar.SECOND); // 截断毫秒,统一为秒级精度
Date nextOneMinute = DateUtils.addMinutes(now, 1); // 未来1分钟的时间
然后使用MP中的查询,查询时间在这个区间内的活动展开情况
QueryWrapper<CardGame> gameQuery = new QueryWrapper<>();
gameQuery.ge("starttime", now) // 开始时间 ≥ 当前时间.le("starttime", nextOneMinute) // 开始时间 ≤ 未来1分钟.eq("status", 0); // 状态为0(新建,未开始)
List<CardGame> pendingGames = gameService.list(gameQuery);
最后一行的List方法相当于下面的sql语句
SELECT * FROM card_game WHERE starttime >= ? AND starttime <= ? AND status = 0
三:遍历活动,将活动的数据缓存到redis中
活动的数据包括:1活动基本信息,活动策略信息,抽奖令牌桶,奖品映射信息
(1)活动基本信息存储
redisUtil.set(RedisKeys.INFO + game.getId(), game, -1);
这里要注意的点就是将这些信息储存成永不过期,因为这种信息基本不变,变了可以直接覆盖原来的信息,所以可以存储成永不过期
(2)活动策略信息
因为活动的服务对象是不同等级的成员,不同等级的成员对应不同的参加次数和中奖次数

QueryWrapper<CardGameRules> rulesQuery = new QueryWrapper<>();
rulesQuery.eq("gameid", game.getId());
List<CardGameRules> rulesList = gameRulesService.list(rulesQuery);for (CardGameRules rule : rulesList) {String userLevelKey = String.valueOf(rule.getUserlevel()); // 用户等级(如:1、2、3)// 存储不同用户等级的中奖次数限制redisUtil.hset(RedisKeys.MAXGOAL + game.getId(), userLevelKey, rule.getGoalTimes());// 存储不同用户等级的参与次数限制redisUtil.hset(RedisKeys.MAXENTER + game.getId(), userLevelKey, rule.getEnterTimes());// 存储不同用户等级的中奖概率redisUtil.hset(RedisKeys.RANDOMRATE + game.getId(), userLevelKey, rule.getRandomRate());
}
(3)抽奖令牌桶
这一步也是比较难想到的一步,下面是它设计的目的和设计方式
- 核心目的:生成 “抽奖令牌”,用于控制抽奖的时间和次数(每个令牌对应一次中奖机会)。
- 令牌设计:
- 基础:活动开始到结束期间的随机时间戳(确保令牌在活动有效期内)。
- 唯一性:时间戳 + 3 位随机数(避免同一毫秒生成重复令牌)。
- 存储:令牌列表按时间排序后存入 Redis(键:
game:tokens:1001),方便后续抽奖时按时间顺序取出(确保公平性)。
(4)缓存奖品映射信息
实现id到奖品名字的映射,过期时间设置为活动结束的时候
// 计算缓存过期时间(活动结束后失效)
long expireTime = (endTime.getTime() - now.getTime()) / 1000; // 秒级for (int i = 0; i < tokenList.size(); i++) {// 随机选择一个奖品与令牌关联(注:此处逻辑可能需根据业务调整,通常应绑定固定奖品)CardGameProduct randomProduct = productList.get(random.nextInt(productList.size()));Long token = tokenList.get(i);String redisKey = RedisKeys.TOKEN + game.getId() + "_" + token; // 键:game:token:1001_xxxxxx// 查询奖品完整信息CardProduct product = cardProductService.getById(randomProduct.getProductid());// 缓存令牌与奖品的映射redisUtil.set(redisKey, product, expireTime);
}
@Component
public class GameTask {private final static Logger log = LoggerFactory.getLogger(GameTask.class);@Autowiredprivate CardGameService gameService;@Autowiredprivate CardGameProductService gameProductService;@Autowiredprivate CardGameRulesService gameRulesService;@Autowiredprivate GameLoadService gameLoadService;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate CardGameProductService cardGameProductService;@Autowiredprivate CardProductService cardProductService;@Scheduled(cron = "0 * * * * ?")public void execute() {log.info("===== 开始执行缓存预热任务,当前时间:{} =====", new Date());try {// 1. 截断毫秒,统一时间精度为秒级 now = DateUtils.truncate(now, Calendar.SECOND);Date now = new Date();now = DateUtils.truncate(now, Calendar.SECOND);// 2. 扩大查询范围到未来1分钟Date nextOneMinute = DateUtils.addMinutes(now, 1);QueryWrapper<CardGame> gameQuery = new QueryWrapper<>();gameQuery.ge("starttime", now) // 开始时间 ≥ 当前时间.le("starttime", nextOneMinute) // 开始时间 ≤ 未来1分钟.eq("status", 0); // 状态为0表示"新建"List<CardGame> pendingGames = gameService.list(gameQuery);log.info("查询到{}个即将开始的活动,时间范围:{}~{},状态:0",pendingGames.size(), now, nextOneMinute);for (CardGame game : pendingGames) {log.info("开始预热活动【ID:{},名称:{}】", game.getId(), game.getTitle());// 2. 缓存「活动基本信息」:永不过期redisUtil.set(RedisKeys.INFO + game.getId(), game, -1);log.info(" 活动基本信息已缓存");// 3. 缓存「活动策略信息」:按用户等级存储中奖/参与次数限制QueryWrapper<CardGameRules> rulesQuery = new QueryWrapper<>();rulesQuery.eq("gameid", game.getId());List<CardGameRules> rulesList = gameRulesService.list(rulesQuery);for (CardGameRules rule : rulesList) {String userLevelKey = String.valueOf(rule.getUserlevel());redisUtil.hset(RedisKeys.MAXGOAL + game.getId(), userLevelKey, rule.getGoalTimes());redisUtil.hset(RedisKeys.MAXENTER + game.getId(), userLevelKey, rule.getEnterTimes());redisUtil.hset(RedisKeys.RANDOMRATE + game.getId(), userLevelKey, rule.getRandomRate());}log.info(" 活动策略信息已缓存,共{}条用户等级规则", rulesList.size());// 4. 生成「抽奖令牌桶」:为每个奖品生成时间戳令牌QueryWrapper<CardGameProduct> productQuery = new QueryWrapper<>();productQuery.eq("gameid", game.getId());List<CardGameProduct> productList = gameProductService.list(productQuery);// 计算总的奖品数量(考虑amount字段)int totalProducts = 0;for (CardGameProduct product : productList) {totalProducts += product.getAmount() != null ? product.getAmount() : 0;}if (totalProducts == 0) {log.warn("活动【{}】无奖品,跳过令牌桶和奖品映射缓存", game.getId());continue;}Date startTime = game.getStarttime();Date endTime = game.getEndtime();long timeDuration = endTime.getTime() - startTime.getTime();List<Long> tokenList = new ArrayList<>();Random random = new Random();// 为每个奖品生成对应数量的时间戳令牌for (CardGameProduct product : productList) {int amount = product.getAmount() != null ? product.getAmount() : 0;for (int i = 0; i < amount; i++) {long randomTime = startTime.getTime() + random.nextInt((int) timeDuration);// 生成带3位随机数的令牌long token = randomTime * 1000 + random.nextInt(999);tokenList.add(token);}}// 对令牌进行排序tokenList.sort(Long::compareTo);// 将令牌列表存入RedisredisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(), tokenList);log.info(" 抽奖令牌桶已生成并缓存,共{}个令牌", tokenList.size());// 5. 缓存「奖品映射信息」long expireTime = (endTime.getTime() - now.getTime()) / 1000; // 过期时间单位为秒for (int i = 0; i < tokenList.size(); i++) {// 随机选择一个奖品与令牌关联CardGameProduct randomProduct = productList.get(random.nextInt(productList.size()));Long token = tokenList.get(i);String redisKey = RedisKeys.TOKEN + game.getId() + "_" + token;// 查询对应的 CardProduct 完整信息CardProduct product = cardProductService.getById(randomProduct.getProductid());// 将完整信息存入 RedisredisUtil.set(redisKey, product, expireTime);}log.info(" 奖品映射信息已缓存,共{}个奖品", productList.size());}} catch (Exception e) {log.error("缓存预热任务执行异常", e);}log.info("===== 缓存预热任务执行完成 =====");}
}
