正文
为了你们能更懂流程,作为暖男的我还是一如既往的给你们准备了伪代码啦:
public String getData(String key){
String data = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(data)){
return data;
}
String lockKey = this.getClass().getName() + ":" + key;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean boo = lock.tryLock(5, 5, TimeUnit.SECONDS);
if (!boo) {
Thread.sleep(200L);
data = getData(key);
}
data = getDataByDB(key);
if (StringUtils.isNotEmpty(data)){
setDataToRedis(key,data);
}
} catch (InterruptedException e) {
}finally {
if (lock != null && lock.isLocked()){
lock.unlock();
}
}
return data;
}
当然,采用互斥锁的方案也是有缺陷的,当缓存失效的时候,同一时间只有一个线程读数据库然后回写缓存,其他线程都处于阻塞状态。如果是高并发场景,大量线程阻塞势必会降低吞吐量。这种情况该如何处理呢?我只能说没什么设计是完美的,你又想数据一致,又想保证吞吐量,哪有那么好的事,为了系统能更加健全,必要的时候牺牲下性能也是可以采取的措施,两者之间怎么取舍要根据实际业务场景来决定,万能的技术方案什么的根本不存在。
缓存雪崩
缓存雪崩也是
key 失效后大量请求打到数据库的异常情况
,不过,跟缓存击穿不同的是,缓存击穿因为指一个热点 key 失效导致的情况,而
缓存雪崩是指缓存中
大批量的数据同时过期
,巨大的请求量直接落到 db 层,引起 db 压力过大甚至宕机
,这也符合字面上的“雪崩”说法。
解决方案
缓存雪崩的解决方案和击穿的思路一致,可以
设置 key 不过期或者互斥锁
的方式。
除此之外,因为是
预防大面积的 key 同时失效,可以给不同的 key 过期时间加上随机值,让缓存失效的时间点尽量均匀
,这样可以保证数据不会在同一时间大面积失效。
redisTemplate.opsForValue().set(Key, value, time + Math.random() * 1000, TimeUnit.SECONDS);
同时还可以
结合主备缓存策略来让互斥锁的方式更加的可靠。
主缓存:
有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值。
备份缓存:
有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。
一般来说,上面三种缓存异常场景问的比较多,了解这几种基本就够了,但有些面试官可能喜欢剑走偏锋,进一步延伸其他的异常情景做询问,以防万一,我们也加个菜,介绍下另外两种常见缓存异常。
缓存预热
缓存预热就是
系统上线后,先将相关的数据构建到缓存中,这样就可以避免用户请求的时候直接查库。
这部分预热的数据主要取决于
访问量和数据量大小。
如果数据的访问量不大的话,那么就没必要做预热,都没什么多少请求了,直接按正常的缓存读取流程执行就好。
访问量大的话,也要看数据的大小来做预热措施。
-
数据量不大的时候,工程启动的时候进行加载缓存动作,这种数据一般可以是电商首页的运营位之类的信息;
-
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
-
数据量太大的时候,优先保证热点数据进行提前加载到缓存,并且确保访问期间不能更改缓存,比如用定时器在秒杀活动前30分钟就把商品信息之类的刷新到缓存,同时规定后台运营人员不能在秒杀期间更改商品属性。
缓存降级
缓存降级是
指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
在项目实战中通常会将部分热点数据缓存到服务的内存中,类似 HashMap、Guava 这样的工具,一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
当然,这样的操作对于业务是有损害的,
分布式系统中很容易就出现数据不一致的问题
,所以,一般这种情况下,我们都优先保证从运维角度确保缓存服务器的高可用性。比如 Redis 的部署采用集群方式,同时做好备份。总之,尽量避免出现降级的影响。
最后
关于缓存的几大异常处理我们就讲解到这了。虽然每种异常我们都给出了解决的方案,但不是说这玩意直接套上就能用了。现实开发过程中还是要根据实际情况来针对缓存做相应措施,比如用布隆过滤器预防缓存穿透虽然很有效,但并不算特别常用。这年头,
防止恶意攻击什么的都是先在运维层面做限制,业务代码层面更多的是对参数和数据做校验。
如果每个使用缓存的地方都要考虑的这么复杂的话,那工作量无疑会更加繁杂,过度设计只会让代码维护起来也麻烦,而且实用性还不一定强,没必要啊。程序员嘛,给自己增添烦恼的事情越少越好,毕竟我们最大的敌人不是996,而是那珍贵的发量啊。