我们一般用缓存可能会遇到这几个问题:
- 数据不一致
- 缓存雪崩
- 缓存穿透
- 缓存并发竞争
为了我们更好解决这些问题,首先我们了解一下概念:
缓存雪崩
问题:
缓存雪崩与缓存击穿不同,缓存雪崩带来的是多个key同时过期,或者redis服务不可用,导致所有的请求全部打到了DB层。
解决方案:
1、建立高可用的Redis集群,使用Cluster加主从的方式,避免单点故障还有全部slot都放到同一个实例上。
2、对于热点数据可以建立本地缓存,即使分布式缓存挂了,也有本地缓存进行兜底。
缓存穿透
问题:
缓存穿透,指的是查询一个不存在的数据,缓存不命中的情况下,DB层查询到的数据又为空,导致Cache层没写缓存。导致请求每次都打到DB层。
解决方案:
1、当查询为空的数据时,缓存的值可以保存一个特殊字符串,比如db_null之类的,并设置一定的过期时间。当下次查询value=db_null的时候,直接就返回空数据了。
不足:缓存太多空值,占用更多空间。建议设置5分钟缓存
2、增加redis bitmap作为过滤器,如果该ID存在时,则可以进行下一步动作。
3、增加布隆过滤器(bloomfilter),bloomfilter存储对应的key是否存在,如果该值存在则表示对应的值不为空。
缓存击穿
问题:
刚好有热点数据缓存过期了,恰好这个时间内有大量请求进来,在建立缓存之前所有请求打到了DB层,导致持久层可能瞬时被压垮。
解决方案:
1、使用分布式锁,请求发现缓存不在后,去查DB前加入分布式锁,保证有且只有一个去DB层查数据,并更新缓存。
2、手动过期,即所有热点的key不设置缓存过期时间,而是把过期时间放到key的value里面。
具体如下:
- 获取缓存:通过key获取value的过期时间,判断是否过期,如果未过期,则直接返回。如果已经过期则继续往下执行。
- 通过后台的一个异步线程去做缓存构建,通过后台的任务去保证有且只有一个线程去查询DB。(可以通过channel去执行,或者消息队列Kafka等方式)
- 同时,虽然value已经过期了,但是value还是照样返回。这样就能保证服务的可用性,但是同时也损失了一些时效性。
3、Golang里面有个singleflight这个文件,可以使用它保证一个节点里面有且只有一个线程去执行,其他线程则等待该请求线程处理完之后获取到真实数据。