众所周知,Redis是一个高性能分布式缓存中间件,在应对高并发的场景时,我们往往需要通过Redis对一些热点数据进行缓存处理。但是,在分布式系统中,在应对高并发情况时,对出现缓存穿透, 缓存击穿,缓存雪崩等问题。那么接下来,我们就来聊一聊在缓存的设计中,如何应对这些高并发问题:
缓存穿透
- 什么是缓存击穿:缓存击穿是指一个不存在的数据,由于缓存是不命中时被动写的,并且处于容错考虑,如果从存储层查不到数据则写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询DB,失去了缓存的意义,这种现象就叫缓存穿透。
- 缓存穿透有什么问题:当流量很大时,可能由于一个或者一系列不存在的key频繁访问我们的系统,造成大量的无效数据访问DB,可能导致DB负载,直接宕机影像其他业务。
- 如何解决:
3.1 业界最简单粗暴的方法是通过一个空的对象来解决缓存穿透的问题(我司的token鉴权就是通过这种方式来防止无效token的):什么意思呢?简单来说,就是当一个key查询db返回的数据为null时(无论是真的数据不存在,还是说系统故障导致当次查询失败),我们仍然会往缓存中插入一个空对象(最好是自定义的一个NullObject),但是它的缓存时间需要设置的较短,避免数据不一致。
3.2 另外一种有效解决缓存穿透问题的方案是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据可能会被这个bitmap拦截掉,从而避免了对DB的查询,减轻DB的压力。
需要注意的是,这里只是可能被拦截,因为布隆过滤器是有一定的错误率的。判断某个key不存在时,则一定不存在;判断某个key存在时,则他有可能存在,也可能不存在。对着这些key,我们就需要再次查询一次缓存或者DB。但是也极大的减轻了DB的压力。
- 什么是布隆过滤器:
缓存击穿
- 什么是缓存击穿:对于一些设置了过期时间的key,如果这些key可能会在某个时间点被超高并发访问,是一种非常热点的数据。如果这些key过期,则请求会直接请求到DB,这种现象就是缓存击穿。
- 缓存击穿有什么影响:当这些非常热点的key在某个时间点过期的时候,恰好在这个时间点对这些key有大量的并发请求过来,此时,这些请求发现缓存过期了,一般就会从后端DB加载数据,并回写到缓存中,这个时候,大量的并发请求可能会瞬间把后端DB压垮。
- 如何解决缓存击穿问题:
3.1 使用互斥锁:这也是业界比较常用的做法。简单地来说,就是在缓存失效的时候(判断拿出来的值为null),不是立即去查询DB,而是先使用缓存工具的某些带成功操作返回值的操作(如redis的setNx),当操作返回成时,在查询DB,同时将查询到的结果写入缓存,否则就重试整个get的方法。
3.2 永不过期:这里的“永不过期”包含两层意思:
- 物理不过期(比较少用,存在一致性问题):从redis上看,不去设置过期时间,这就保证了不会出现热点key过期的问题。也就是物理不过期。
- 逻辑不过期:从功能上看,如果真的设置成不过期,那么缓存就成了静态数据,当DB使了。所以我们可以把过期时间存在key对应的value里面,如果发现要过期了,通过一个后台的异步线程进行缓存的重新构建,也就是逻辑不过期。这样可以在一定程度上保证一致性问题。但是在缓存重建的过程中,缓存读到的还是旧数据,导致不一致。
缓存雪崩
- 什么是缓存雪崩:缓存雪崩是指我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬间压力过重导致宕机。常见的如缓存服务器宕机,大量热点数据同时失效等。
- 如何解决缓存雪崩问题:
业界流传的方案是:最好的解决缓存雪崩的方案,就是不发生雪崩。
2.1 大多数系统设计者通常都是考虑通过加锁或者队列的方式保证缓存的单线程(单进程)写,从而避免失效时,大量的并发请求打到DB上,导致服务宕机。
2.2 另一种简单的方案就是:将缓存失效时间进行离散,即在原有的失效时间的基础上,加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就一定程度上降低了大量key同时过期失效的问题。这样就解决了大量热点数据同时失效的问题,然后通过redis的主从复制+哨兵机制,或者redis集群模式,可以有效解决缓存服务器宕机引发的缓存雪崩,
总结
针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。任何的方案都是有取舍的,不可能存在一站式解决所有问题的方案。
最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据业务分析。通常采用LRU策略处理缓存溢出问题即可。
Redis的RDB和AOF持久化策略,可以保证一定情况下的数据不丢失。