本文已收录于专栏 《Redis从入门到进阶》
专栏前言
本专栏开启,目的在于帮助大家更好的掌握学习Redis
,同时也是为了记录我自己学习Redis
的过程,将会从基础的数据类型开始记录,直到一些更多的应用,如缓存击穿还有分布式锁以及Redis
持久化等。希望大家有问题也可以一起沟通,欢迎一起学习,对于专栏内容有错还望您可以及时指点,非常感谢大家 。
目录
- 专栏前言
- 1.缓存的三大问题
- 2.缓存雪崩
- 2.1 什么是缓存雪崩?
- 2.如何解决缓存雪崩?
- 3.缓存穿透
- 3.1 什么是缓存穿透?
- 3.2 如何解决缓存穿透?
- 4. 缓存击穿
- 4.1 什么是缓存击穿?
- 4.2 如何解决缓存击穿?
1.缓存的三大问题
话说学习缓存,缓存雪崩、击穿、穿透肯定是绕不开的一个话题,不仅在开发中我们需要注意这个问题,在面试中更是面试官有关Redis
必问的面试题,可以说我们对这三个场景我们必须了解其中的差异,以及各自的解决方案。
2.缓存雪崩
2.1 什么是缓存雪崩?
缓存雪崩指的是在同一时间,大量的Key
同时失效,导致大量的请求直接绕开了我们的Redis
直接打到数据库,数据库一下顶不住直接挂彩了,这就是缓存雪崩的场景。
那有的人肯定好奇,咦?怎么会那么巧大量的Key
同时失效,考虑一个秒杀场景,比如双十一的时候,我们在晚上十一点集中上架一批商品,此时缓存过期时间设置为一小时,那么一到十二点这批商品的缓存就全部过期了,如果此时成千上万的用户想购买这些商品发送大量请求,就会导致数据库的压力上升从而可能压垮数据库。
同一时间大量 Key
失效,就会导致Redis
直接如同隐身了一样,那对于双十一这种数以千万级别甚至上亿量级的请求来到数据库,那后果就是灾难性的。一旦你一个库被打倒,那么其他的库可能也会收到影响,导致瞬间都挂掉了。你一重启用户又把你干崩,等你真正修好的时候,可能用户早就睡着了,还在心里吐槽一句什么垃圾产品。
当然,如果Redis
宕机了,那么显然也会触发缓存雪崩的情况。
2.如何解决缓存雪崩?
讨论完了案发场景,那我们该如何解决缓存雪崩了。
- 过期时间添加随机值。
那就对症下药嘛,既然你是大量的key
同时过期导致的,那我就尽量让你不一起过期,所以我们在批量添加缓存的时候,可以给过期时间添加一个随机值,使得Key
过期的时间尽量分散,这样保证缓存不会大面积的同时失效。 - 进行集群部署
集群部署的情况下,我们就无须担心某一台Redis
宕机导致触发缓存雪崩,也可以将热点数据均匀分布在不同Redis
库中,来避免全部失效的问题。 - 热点数据永不过期
我们也可以让热点数据永不过期,只进行更新的操作,这也可以避免缓存雪崩。但可能会带来数据不一致的问题。
3.缓存穿透
3.1 什么是缓存穿透?
缓存穿透指的是对于一些缓存和数据库中都不存在的数据,而用户却不断对该数据进行请求,如果你的数据库甚至没有建立索引,那么数据库还会进行全表扫描,压力更大。每次数据库都需要去进行查,然后查不到,然后又继续查,然后又查不到,然后又…最终数据库卒,这就是缓存穿透。
此时显然用户是一个攻击者,比如我们数据库id
都是从1
开始自增的,它可以故意访问一个负数的id
,然后不断请求,如果是我们个人搭建的一些小网站,对这种行为没有预防,只需要用 postman 就可以干崩你的网站。
3.2 如何解决缓存穿透” />import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.ValueOperations;import org.springframework.data.redis.core.RedisOperations;import org.springframework.data.redis.core.RedisConnectionUtils;import org.springframework.data.redis.RedisConnectionFailureException;import java.util.concurrent.locks.ReentrantLock;@Componentpublic class RedisCache { private final static String CACHE_PREFIX = "my_cache_"; private final static int EXPIRE_TIME_SECONDS = 60; private final ReentrantLock lock = new ReentrantLock();@Autowired private StringRedisTemplate stringRedisTemplate; public Object get(@NotBlank String key) { String cacheKey = CACHE_PREFIX + key; //1.先查询缓存 ValueOperations<String, String> ops = redisTemplate.opsForValue(); String value = ops.get(cacheKey); //2.缓存未命中 if (value == null) { //3.尝试获取锁 lock.lock(); try { //4.双重校验 value = ops.get(cacheKey); if (value == null) { //5.查询数据库 value = fetchDataFromDatabase(key); //6.写回redis ops.set(cacheKey, value, EXPIRE_TIME_SECONDS); } } catch (Exception e) { System.out.println("出现异常"); } finally { //释放锁 lock.unlock(); } } return value; } private String fetchDataFromDatabase(String key) { System.out.println("查询数据库"); return key; }}
import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.ValueOperations;import org.springframework.data.redis.core.RedisOperations;import org.springframework.data.redis.core.RedisConnectionUtils;import org.springframework.data.redis.RedisConnectionFailureException;import java.util.concurrent.locks.ReentrantLock;@Componentpublic class RedisCache { private final static String CACHE_PREFIX = "my_cache_"; private final static int EXPIRE_TIME_SECONDS = 60; private final ReentrantLock lock = new ReentrantLock();@Autowired private StringRedisTemplate stringRedisTemplate; public Object get(@NotBlank String key) { String cacheKey = CACHE_PREFIX + key; //1.先查询缓存 ValueOperations<String, String> ops = redisTemplate.opsForValue(); String value = ops.get(cacheKey); //2.缓存未命中 if (value == null) { //3.尝试获取锁 lock.lock(); try { //4.双重校验 value = ops.get(cacheKey); if (value == null) { //5.查询数据库 value = fetchDataFromDatabase(key); //6.写回redis ops.set(cacheKey, value, EXPIRE_TIME_SECONDS); } } catch (Exception e) { System.out.println("出现异常"); } finally { //释放锁 lock.unlock(); } } return value; } private String fetchDataFromDatabase(String key) { System.out.println("查询数据库"); return key; }}
需要注意我们这里的场景是单机的情况下,如果是分布式环境的话我们就得使用分布式锁了,这里我们使用synchronized
和lock
都是可以的。有关分布式锁我们后续文章会进行讲解
- 热点数据永不过期
同样因为是数据过期的问题,那我们也可以考虑设置热点数据永不过期,当后台更新数据的同时更新缓存中的数据,当然可能会带来数据不一致的问题,适用于不严格要求缓存一致性的场景。