背景
- 之前提到过在数据库查询中, 我们可以使用缓存来提高查询效率, 减少数据库的压力.
缓存的方式
-
本地缓存
- 基于内存的缓存, 如HashMap, LinkedHashMap, Guava Cache等.
-
远程缓存
- 分布式缓存: 如Redis, Memcached, Etcd(云原生架构的分布式缓存)等.
- Java进程缓存: 如Ehcache(单机), Caffeine(号称最快的缓存库), Google Guava等.
Redis缓存
- 在分布式缓存中, Redis是最常用的缓存服务器.
- 了解redis的基本概念和使用方法, 可以查看文章: redis知识
使用Redis缓存注意事项
-
设计缓存的key
- 一般来说, 我们需要缓存的数据的key应该是唯一的, 并且能够反映出数据的内容.
- 格式: systemId:moduleId:func:options(系统ID:模块ID:功能:参数, 可以根据实际情况进行调整, 原则是不要和其他数据冲突)
-
缓存的过期时间
- 一定要设置过期时间, redis的内存不能无限扩大, 所以需要设置合理的过期时间.
- 缓存的过期时间应该根据数据的重要性和更新频率来设置.
- 对于不经常更新的数据, 可以设置较短的过期时间, 如10分钟.
- 对于经常更新的数据, 可以设置较长的过期时间, 如1天.
- 对于一些重要的数据, 可以设置更长的过期时间, 如30天.
问题延伸
-
问题描述
- redis缓存解决了查询效率的问题, 但是缓存的命中率并不一定高, 比如缓存删除之后,第一个查询还是会到数据库查询, 数据量大的话, 用户还是会看到延迟.
-
解决方案
- 可以使用缓存预热
缓存预热
- 缓存预热是指将热点数据提前加载到缓存中, 这样当第一个用户请求到来时, 缓存就有数据可以直接返回, 避免了延迟.
-
缓存预热的优点
- 降低了首次加载的延迟, 提高了用户体验
- 缓存预热后, 缓存命中率提高, 降低了数据库的压力
-
缓存预热的缺点
- 增加开发成本, 需要额外的开发、设计
- 预热的时机和时间如果设置不当, 可能会造成缓存数据不正确.
- 缓存预热的缓存数据量不能太大, 否则会占用过多的内存.
-
缓存预热的实现方式
- (1) 定时缓存预热: 定时任务定期访问数据库, 将热点数据加载到缓存中.
- (2) 模拟触发(手动触发): 在代码中主动访问数据库, 将热点数据加载到缓存中.
-
定时缓存预热的实现方式
- (1) Spring Scheduler(spring boot 默认整合了, 最常用)
- (2) Quartz(独立于 Spring 存在的定时任务框架)
- (3) XXL-Job 之类的分布式任务调度平台(界面 + sdk), 非常值得学习, https://github.com/xuxueli/xxl-job
使用Spring Scheduler实现定时缓存预热
-
主类添加注解@EnableScheduling
@EnableScheduling // 开启定时任务 public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
-
给要定时执行的方法添加 @Scheduling 注解,指定 cron 表达式或者执行频率
@Component @Slf4j public class PreCacheJob { @Resource private UserService userService; @Resource private RedisTemplate<String, Object> redisTemplate; // 重点用户 private List<Long> mainUserList = Arrays.asList(1L); // 每天执行,预热推荐用户 @Scheduled(cron = "0 45 14 * * *") //自己设置时间测试 public void doCacheRecommendUser() { //查数据库 for (Long userId : mainUserList) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper); String redisKey = String.format("hjx:user:recommend:%s", userId); ValueOperations valueOperations = redisTemplate.opsForValue(); //写缓存,30s过期 try { valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("redis set key error", e); } } } }