缓存预热
问题背景
在处理大量数据查询时,MySQL 数据查询速度可能会变慢,影响系统的性能和用户体验。为了提高查询速度,使用缓存是一种常见的解决方案,即提前将数据取出并保存好,通常存储在内存中以加快访问速度。
缓存的实现方式有多种,包括:
- Redis:分布式缓存,具有高扩展性和高性能。
- Memcached:分布式缓存,但功能相对较简单。
- Ehcache:单机缓存,适用于小规模应用。
- 本地缓存(Java Map):简单易用,但在分布式环境中存在局限性。
- Caffeine:Java 内存缓存,性能较高。
- Google Guava:提供了一些缓存相关的工具类。
Redis缓存
在使用 Spring Data Redis 进行缓存预热时,需要设计合理的缓存 Key,以避免不同用户的数据冲突。例如,可以使用systemId:moduledId:func
的格式,并确保其唯一性。
@GetMapping("/recommend")
public Result<Page<User>> recommendUser(int pageSize, int pageNum, HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
String redisKey=String.format("ikun:user:recommend:%s",loginUser.getId());
ValueOperations valueOperations = redisTemplate.opsForValue();
Page<User> userPage = (Page<User>) valueOperations.get(redisKey);
if (userPage!=null){
log.info("get recommend user from redis");
return ResultUtils.success(userPage);
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> userList = userService.page(new Page<>(pageNum, pageSize), queryWrapper);
try {
valueOperations.set(redisKey,userList);
} catch (Exception e) {
log.error("redis set error:{}",e.getMessage());
}
return ResultUtils.success(userList);
}
然而,即使使用了缓存,第一个用 户访问时仍然可能会遇到较慢的情况。为了解决首次访问系统的用户主页加载过慢的问题,可以使用 Spring Scheduler 定时任务来实现缓存预热,并通过分布式锁保证多机部署时定时任务不会重复执行。同时,在缓存时可以只给重要客户(如 VIP)进行缓存,以提高缓存的效率和价值。
缓存预热
- 定时任务的实现:
- Spring Scheduler:Spring Boot 默认整合了 Spring Scheduler,使用方便。
- Quartz:独立于 Spring 存在的定时任务框架,功能强大,但配置相对复杂。
- 注意点:
- 缓存预热的意义:当新增数据较少,而总用户数量较多时,缓存预热可以显著提高系统的性能,减少用户的等待时间。
- 缓存的空间管理:缓存的空间不能太大,需要合理规划,预留给其他重要的缓存数据。
- 缓存数据的周期:根据数据的变化频率和重要性,合理设置缓存数据的更新周期,以保证数据的及时性和准确性。
使用定时任务:
package com.yunfei.Job;
/**
* 缓存预热任务
*/
@Component
@Slf4j
public class PreCacheJob {
@Resource
private RedisTemplate<String, ObjectKey> redisTemplate;
private List<Long> mainUserList = Arrays.asList(1L);
@Resource
private RedissonClient redissonClient;
//每天的23点59执行
@Scheduled(cron = "0 59 23 * * *")
public void doCacheRecommendUser() {
String redisKey1 = "ikun:precacheJob:docache:lock";
RLock lock = redissonClient.getLock(redisKey1);
try {
if (lock.tryLock(0, 30000, TimeUnit.MICROSECONDS)) {
for (Long userId : mainUserList) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> page = userService.page(new Page<>(1, 10), queryWrapper);
String redisKey = String.format("ikun:user:recommend:%s", userId);
ValueOperations valueOperations = redisTemplate.opsForValue();
try {
valueOperations.set(redisKey, page.getRecords());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//只能释放自己的锁
if (lock.isHeldByCurre ntThread()) {
lock.unlock();
}
}
}
}
在这个代码中,使用了 Redisson 的分布式锁来确保在多机部署时,定时任务不会重复执行。同时,通过遍历重要用户列表,将其相关数据提前缓存到 Redis 中,以提高用户首次访问时的速度。