缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。
缓存雪崩的事前事中事后的解决方案如下:
-
事前:
尽量保证整个 Redis 集群的高可用性;
选择合适的内存淘汰策略。比如将缓存失效时间分散开。
-
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 雪崩。
-
事后:利用 Redis 持久化机制保存的数据快速恢复缓存。
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,数据库可能就挂掉了,有人利用不存在的键频繁攻击我们的应用,这就是漏洞。
- 简单粗暴的方法:如果一个查询返回的数据为空(不管数据是否存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过 5 分钟。
- 布隆过滤器:布隆过滤器能够以极小的空间开销解决海量数据判重问题。一个一定不存在的数据会被该过滤器拦截掉,从而避免了对底层存储系统的查询压力。
缓存击穿是指某个键非常热点,访问非常频繁,处于集中式高并发访问的情况。当缓存在某个时间点过期的时,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
- 将热点数据设置为永远不过期。不会出现热点键过期问题。
- 基于 Redis 或 Zookeeper 实现互斥锁。根据键获得的值为空时,先加锁,然后从数据库加载数据,加载完毕后再释放锁。其他线程发现获取锁失败,等待一段时间后重试。
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。
数据库和缓存双写,就必然会存在不一致的问题。答这个问题,如果对数据有强一致性要求,则数据不能放入缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。
存在的问题
数据更新,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后更新数据的程序完成了数据库的修改,此时,数据库和缓存中的数据又不一致了。
并发场景下分析:
- 线程A删除了缓存
- 线程B查询,发现缓存已不存在
- 线程B去数据库查询得到旧值
- 线程B将旧值写入缓存
- 线程A将新值写入数据库
所以也会导致数据库和缓存不一致的问题。
解决方案
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
问题:为什么是删除缓存,而不是更新缓存?
原因:如果每次更新了数据库,都要更新缓存【这里指的是频繁更新的场景,这会耗费一定的性能】,倒不如直接删除掉。等再次读取时,缓存里没有,那再去数据库找,在数据库找到再写到缓存里边(体现懒加载)。
举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 100 次,那么缓存更新 100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它在需要被使用时才重新计算。
Cache Aside Pattern 最经典的缓存结合数据库读写的模式。
- 读的时候,先读缓存,缓存中没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
存在的问题
先修改数据库,再删除缓存。
如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现不一致。
如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:
- 缓存刚好失效
- 线程A查询数据库,得一个旧值
- 线程B将新值写入数据库
- 线程B删除缓存
- 线程A将查到的旧值写入缓存
要达成上述情况,还是说一句概率特别低:
因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
读请求和写请求串行化,串入到一个内存队列。读串行化可保证一定不会出现双写一致性的情况,但会导致系统吞吐量大幅降低,需要比正常情况下多几倍的机器去支持线上一个请求,若不是严格要求双写一致性,则最好不要使用。