背景
在学习缓存雪崩的时候,了解到有二级缓存和随机TTL两个解决方案,但是在学习之后,个人认为二级缓存本质上还是利用两层缓存的过期时间不一致来实现缓存过期时间随机化,这不就是和随机TTL一样吗,故有了这篇思考,如有不对,欢迎指正
缓存雪崩是什么
缓存雪崩指的是大量的缓存同时失效(过期)导致大量的请求直接到了数据库,数据库瞬间受到了很大的压力,可能导致系统崩溃
或者是缓存层直接不提供服务宕机导致大量请求直接打到数据库上,使得数据库承受很大压力
其中缓存雪崩解决方案有多种,其中两种为
- 二级缓存
- 设置随机ttl(随机过期时间)
二级缓存是什么?
二级缓存指的是将两个不同层次的缓存结合起来使用,是一种组合型分组的缓存结构,最常见的就是Redis+本地缓存(如Caffeine)。目的是通过分层设计,在内存与持久化缓存之间分担压力,提高系统的容错能力与性能。(一般是采取热点数据存储到本地缓存,非热点数据存储到redis种)
常见的一个请求流程图如下:
首先先查询一级缓存,随后如果读取不到的话就读取二级缓存,再没有就去数据库查询并且将找到的数据写入一二级缓存
代码示例
// 根据店铺 ID 查询店铺信息public String queryShopById(Long id) {String cacheKey = CACHE_SHOP_KEY + id;// 1. 首先查询 Caffeine 本地缓存String shop = caffeineCache.getIfPresent(cacheKey);if (shop != null) {System.out.println("从Caffeine缓存中查询到数据...");return shop;}// 2. 如果 Caffeine 本地缓存没有,从 Redis 查询shop = redisTemplate.opsForValue().get(cacheKey);if (shop != null) {System.out.println("从Redis缓存中查询到数据...");// Redis 命中,放入 Caffeine 本地缓存caffeineCache.put(cacheKey, shop);return shop;}// 3. 如果 Redis 和 Caffeine 都没有,从数据库查询shop = getShopFromDatabase(id);if (shop != null) {// 数据库查询成功,将数据放入 Redis 和 Caffeine 缓存redisTemplate.opsForValue().set(cacheKey, shop, 10, TimeUnit.MINUTES); // 设置 Redis 缓存有效期caffeineCache.put(cacheKey, shop); // 将数据放入 Caffeine 本地缓存return shop;}return "店铺不存在";}
二级缓存如何解决缓存雪崩的问题
(1) 缓存过期时间不一致,避免大规模并发请求(核心)
- 为了避免缓存雪崩问题,Redis 缓存和 Caffeine 缓存的过期时间可以设置不同,而且本地缓存 Caffeine 还支持自动清除过期的缓存。这种不同的过期策略可以避免大量缓存同时过期。
- 避免全量缓存同时失效:通过设计合理的过期时间,避免 Redis 和 Caffeine 缓存同时在同一时刻过期。比如,Redis 缓存设置为较长的过期时间(例如 10 分钟),而 Caffeine 的过期时间较短(例如 1 分钟)。这样即使 Redis 缓存失效,本地缓存仍然能继续工作,避免了缓存雪崩的发生。(caffeine可以通过设置expireAfter:基于个性化定制的逻辑来实现过期处理,灵活设置过期时间)
(2) Redis 缓存失效时,Caffeine 能接管
- Redis 缓存失效时,由于本地缓存(Caffeine)的存在,查询操作首先会尝试从 Caffeine 获取数据,这样能够在 Redis 缓存失效期间保证一定的服务可用性,并避免所有请求都落到数据库上。
(3) Caffeine 本地缓存减少对 Redis 的压力
- Caffeine 作为本地缓存,存储的是每个节点本地的数据,能够极大地减少对 Redis 的访问。
- 即使 Redis 缓存失效(比如 Redis 发生故障或缓存过期),本地缓存 Caffeine 仍然可以继续提供服务,避免了缓存失效后直接查询数据库的压力。
随机ttl是什么?
随机 TTL(Time to Live)是设置缓存过期时间的一种策略,它通过给每个缓存设置一个 随机的过期时间,来避免缓存过期时间集中在同一时刻。从而避免了大量缓存同时过期,避免了大量请求同时击中数据库的缓存雪崩问题。
一种常用的方案就是在集体的过期时间上各个添加一个随机的时长从而实现随机ttl
随机ttl如何解决的缓存雪崩?
即时多个原本的缓存项的原本的过期时间相同,经过随机化后,它们的实际过期时间就不同了,减少了集中过期概率
当缓存项过期,新的请求触发从数据库加载数据并更新缓存,避免了大规模同时从数据库加载数据
示例代码
public class CacheExample {private static final String CACHE_KEY = "shop_info:";private static final int MIN_TTL = 5; // 最小 TTL 时间(秒)private static final int MAX_TTL = 20; // 最大 TTL 时间(秒)private static Jedis jedis = new Jedis("localhost", 6379); // Redis 连接// 模拟查询数据库获取数据private static String queryDatabase(Long id) {return "Shop Info for ID: " + id;}// 获取缓存数据public static String getShopInfo(Long id) {String cacheKey = CACHE_KEY + id;// 尝试从 Redis 获取缓存数据String cachedData = jedis.get(cacheKey);if (cachedData != null) {System.out.println("从缓存获取数据: " + cachedData);return cachedData;}// 如果缓存不存在,查询数据库String data = queryDatabase(id);// 设置缓存时,使用随机的 TTL(过期时间)int ttl = new Random().nextInt(MAX_TTL - MIN_TTL + 1) + MIN_TTL;jedis.setex(cacheKey, ttl, data);System.out.println("从数据库获取数据并缓存: " + data);return data;}// 模拟调用缓存查询public static void main(String[] args) {// 查询 shop 1System.out.println(getShopInfo(1L));// 查询 shop 2System.out.println(getShopInfo(2L));}
}
但是,经过看过两种实现,其实仔细一看好像似乎随机TTL和二级缓存的本质都是避免缓存同时过期来解决缓存雪崩的问题,保证缓存不会大量同一时刻失效,从而减少数据库的访问压力,但是二者并非一致
随机TTL VS 二级缓存
1. 随机 TTL 优势:
- 简单实现:相比于二级缓存方案,随机 TTL 不需要引入额外的缓存系统,只需调整缓存的过期策略即可,配置和实现较为简单。
- 降低缓存过期集中性:通过随机化每个缓存项的过期时间,解决了大量缓存同时过期的问题,减少了对数据库的压力。
2. 二级缓存优势:
- 更高的可用性:二级缓存引入了本地缓存(如 Caffeine)和分布式缓存(如 Redis),即使 Redis 出现故障,本地缓存 Caffeine 仍然能继续提供数据服务,确保系统高可用。
- 更高的性能:本地缓存 Caffeine 可以极大降低 Redis 的访问压力,提升系统响应速度。
- 数据一致性和实时更新:二级缓存可以通过合理的缓存更新机制和缓存失效策略,保持数据的一致性和及时更新,避免缓存和数据库的不同步问题。
二者的核心区别:
缓存的可靠性和冗余
- 随机ttl只是通过在缓存失效时间引入随机性避免缓存雪崩问题,但是这种方法只依赖单一缓存层,如果缓存层出现问题,整个缓存机制失效,所有请求直接访问数据库
- 二级缓存而是通过两层缓存系统提高缓存可靠性和容错能力,即时一级缓存失效,应用依然能够依赖另外一层缓存提供数据,避免了对数据库的直接访问,意味着即使分布式缓存(如 Redis)失效,应用也可以通过本地缓存继续提供数据,保证系统的高可用性。
性能的优化:
本地缓存的延迟比redis更短,而且内存访问速度极快,本地缓存是 非常有效的性能优化手段
总结
虽然 随机 TTL 和 二级缓存 都是解决 缓存雪崩 的有效手段,但它们的设计目标和适用场景有所不同。随机 TTL 简单有效,适用于单一缓存层的情况,尤其是 Redis。二级缓存通过结合 Redis 和本地缓存来增强系统的 容错性、高可用性、性能优化 ,适用于更复杂和高并发的场景。