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

java
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到奖品名字的映射,过期时间设置为活动结束的时候
java
// 计算缓存过期时间(活动结束后失效)
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);
}
java
@Component
public class GameTask {
private final static Logger log = LoggerFactory.getLogger(GameTask.class);
@Autowired
private CardGameService gameService;
@Autowired
private CardGameProductService gameProductService;
@Autowired
private CardGameRulesService gameRulesService;
@Autowired
private GameLoadService gameLoadService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private CardGameProductService cardGameProductService;
@Autowired
private 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);
// 将令牌列表存入Redis
redisUtil.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());
// 将完整信息存入 Redis
redisUtil.set(redisKey, product, expireTime);
}
log.info(" 奖品映射信息已缓存,共{}个奖品", productList.size());
}
} catch (Exception e) {
log.error("缓存预热任务执行异常", e);
}
log.info("===== 缓存预热任务执行完成 =====");
}
}