缓存穿透、缓存击穿、缓存失效
1. 使用缓存存储数据的步骤
1、先查询缓存,如果没有数据,再去查询数据库
2、查询完数据库之后,如果数据不为空,再将结果写入缓存
2. 缓存穿透
1. 什么叫缓存穿透?
- 一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。
也就是说,对不存在的key进行高并发访问,导致数据库压力瞬间增大,这就叫做缓存穿透。 - 如何解决?
- 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
- 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。(布隆表达式)
3. 缓存雪崩
1. 什么叫缓存雪崩?
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
- 如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)
4. 缓存击穿
- 什么叫缓存击穿?
-
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
-
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
-
- 如何解决?
- 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
if(redis.sexnx()==1){ //先查询缓存 //查询数据库 //加入缓存 }
- 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
5. 数据库和缓存双写一致性问题
- 一般来说,在读取缓存方面,我们都是先读取缓存,再读取数据库的。
但是,在更新缓存方面,我们是需要先更新缓存,再更新数据库?还是先更新数据库,再更新缓存?还是说有其他的方案?
一、先更新数据库再更新缓存(不建议使用)
- 操作步骤(线程A和线程B都进行更新操作):
1.线程A更新了数据库
2.线程B更新了数据库
3.线程B更新了缓存
4.线程A更新了缓存 - 问题1:脏读
- 问题2:浪费性能
二、先更新数据库再删除缓存
-
操作步骤(线程A更新、线程B读)
请求A进行写操作,删除缓存
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A将新值写入数据库 -
解决方案1:延时双删策略,伪代码如下:
public void write(String key, Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(1000); redis.deleKey(key); }
-
解决方案2:使用消息队列
三、先删除缓存再更新数据库
- 操作步骤
1.用户A删除缓存失败
2.用户A成功更新了数据
或者
1.用户A删除了缓存;
2.用户B读取缓存,缓存不存在;
3.用户B从数据库拿到旧数据;
4.用户B更新了缓存;
5.用户A更新了数据。 - 问题:脏数据
- 解决方案1:设置缓存有效时间(最简单)
- 解决方案2:使用消息队列