用了缓存之后,有哪些常见问题?
常见的问题,可列举如下:
写入问题
缓存何时写入?并且写时如何避免并发重复写入?
缓存如何失效?
缓存和 DB 的一致性如何保证?
经典三连问
如何避免缓存穿透的问题?
如何避免缓存击穿的问题?
如果避免缓存雪崩的问题?
如何避免缓存”穿透”的问题?
缓存穿透:是指查询一个一定不存在缓存中的数据,缓存不命中就会对到DB去查询,并且处于容错考虑,如果DB中不存在该数据,就不会更新缓存,这样每条请求都会去查一遍DB,这样缓存就会失去其意义。
如果存在黑客攻击,大量的请求打进来,可能mysql就会崩掉。
当然缓存击穿不一定是攻击,也可能是自己写的程序有问题,疯狂的请求不存在的数据,又或者无脑的爬虫请求。
如何去处理:
1.将不存在数据缓存起来
将不存在DB的书库,更新到缓存中,当然,要使用特殊的标识将该数据和正式的数据区分开来,做另外的请求反馈,并且还要配置好该缓存数据的过期策略,一般不要超过五分钟。
2.布隆过滤器(BloomFilter)
在缓存服务的基础上,构建BloomFilter布隆过滤器,在 BloomFilter 中存储对应的 KEY 是否存在,如果存在,说明该 KEY 对应的值不为空
这里流程即为:
布隆过滤器:如果存在对应的KEY值则进行下一步,否则直接返回。
数据缓存,如果存在KEY值,返回数据,否则直接返回。
查询到DB:如果存在则返回数据,并更新缓存。
这里需要注意:
BloomFilter存在误判,总而言之,就是不同过滤器中,存在不一定存在,不存在则一定不存在。
因为BloomFilter不能删除数据的特点,数据如果刚开始存在,后来删除了,那么BloomFilter中该数据存在,但DB不存在,会出现误判的情况。
当然,使用 BloomFilter 布隆过滤器的话,需要提前将已存在的 KEY ,初始化存储到【BloomFilter 缓存】中。
并且常用的缓存 Redis 默认不支持 BloomFilter 数据结构。
实际情况下,使用方案二比较多(BloomFilter)。因为,相比方案一来说,更加节省内容,对缓存的负荷更小。
如果避免缓存雪崩的问题?
缓存雪崩:由于某些原因导致无法提供服务(例如:缓存挂了),所有请求到达DB,导致DB负载严重,最终DB挂掉
如何解决雪崩
1.实现缓存高可用,如果其中一个缓存挂掉了,还有备用,
假设我们使用 Redis 作为缓存,则可以使用 Redis Sentinel 或 Redis Cluster 实现高可用。
2.引用本地缓存
即使分布式缓存服务挂掉了,也可以将DB查询到的数据缓存到本地,不至于一下将DB暴露出来。
当然引入本地服务,需要考虑实时性,
1.可以引入消息队列,在数据更新时,发布数据更新的消息;而进程中有相应的消费者消费该消息,从而更新本地缓存。
2.设置较短的过期时间,过期时从 DB 重新拉取。
每个进程可能会本地缓存相同的数据,导致数据浪费?
方案一,需要配置本地缓存的过期策略和缓存数量上限。
如果我们使用 JVM ,则可以使用 Ehcache、Guava Cache 实现本地缓存的功能。
3.请求 DB 限流
通过限制 DB 的每秒请求数,避免把 DB 也打挂了。这样至少能有两个好处:
可能有一部分用户,还可以使用,系统还没死透。
未来缓存服务恢复后,系统立即就已经恢复,无需再处理 DB 也挂掉的情况。
当然,被限流的请求,我们最好也要有相应的处理,走【服务降级】,提供一些默认的值,或者友情提示,甚至空白的值也行。
如果我们使用 Java ,则可以使用 Guava RateLimiter、Sentinel、Hystrix 实现限流的功能。
如何避免缓存"击穿"的问题?
缓存击穿,是指某个极度“热点”数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求过来,这些请求发现缓存过期一般都会从 DB加载数据并回设到缓存,但是这个时候大并发的请求可能会瞬间 DB 压垮。
对于一些设置了过期时间的 KEY ,如果这些 KEY 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑这个问题。
如何解决:
1.使用互斥锁
请求发现缓存不存在后,去查询DB,使用分布式锁,保证有且只有一个线程去查询 DB ,并更新到缓存
流程如下
1、获取分布式锁,直到成功或超时。如果超时,则抛出异常,返回。如果成功,继续向下执行。
2、获取缓存。如果存在值,则直接返回;如果不存在,则继续往下执行。😈 因为,获得到锁,可能已经被“那个”线程去查询过 DB ,并更新到缓存中了。
3、查询 DB ,并更新到缓存中,返回值。
2.使用手动过期
缓存上从不设置过期时间,功能上将过期时间存在 KEY 对应的 VALUE 里。
流程如下:
1、获取缓存。通过 VALUE 的过期时间,判断是否过期。如果未过期,则直接返回;如果已过期,继续往下执行。
2、通过一个后台的异步线程进行缓存的构建,也就是“手动”过期。通过后台的异步线程,保证有且只有一个线程去查询 DB。
3、同时,虽然 VALUE 已经过期,还是直接返回。通过这样的方式,保证服务的可用性,虽然损失了一定的时效性。(无法保证缓存一致性)
区别:
缓存"击穿"和缓存"雪崩"的区别在于,前者针对某一 KEY 缓存,后者则是很多 KEY 。
缓存"击穿"和缓存"穿透"的区别在于,这个 KEY 是真实存在对应的值的。