缓存设计
参考文献
- 300分钟吃透分布式缓存
- 七大缓存经典问题
- 缓存和数据库一致性问题,看这篇就够了
七大缓存经典问题
缓存穿透(一穿到底)
原因分析
-
缓存穿透存在的原因:用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
- 如用户访问的是一个不存在的 key,查 DB 返回空(即一个 NULL),那就不会把这个空写回 cache.那以后不管查询多少次这个不存在的 key,都会Cache Miss,都会查询 DB.整个系统就会退化成一个 “前端 + DB“的系统,由于 DB 的吞吐只在Cache 的 1%~2% 以下,如果有特殊访客,大量访问这些不存在的 key,就会导致系统的性能严重退化,影响正常用户的访问.
-
缓存穿透会发⽣在什么时候呢?⼀般来说,有两种情况
- 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
- 恶意攻击:专⻔访问数据库中没有的数据。
处理办法
-
验证拦截
- 接口层进行校验拦截,对于一些可预知的非法参数进行拦截,如查询ID字段,传入的值为负值的情况;
-
回种空值
- 查询这些不存在的数据时,第一次查 DB,虽然没查到结果返回 NULL,仍然记录这个 key 到缓存,只是这个 key 对应的 value 是一个特殊设置的值.但是如果特殊访客持续访问大量的不存在的 key,这些 key 即便只存一个简单的默认值,也会占用大量的缓存空间,导致正常 key 的命中率下降.所以进一步的改进措施是,对这些不存在的 key 只存较短的时间,让它们尽快过期;或者将这些不存在的 key 存在一个独立的公共缓存,从缓存查找时,先查正常的缓存组件,如果 miss,则查一下公共的非法 key 的缓存,如果后者命中,直接返回,否则穿透 DB,如果查出来是空,则回种到非法 key 缓存,否则回种到正常缓存.
-
使用布隆过滤器
- 构建一个
BloomFilter
缓存过滤器,记录全量数据,这样访问数据时,可以直接通过BloomFilter
判断这个 key 是否存在,如果不存在直接返回即可,根本无需查缓存和 DB.但是BloomFilter
要缓存全量的 key,这就要求全量的 key 数量不大,10 亿条数据以内最佳,因为 10 亿 条数据大概要占用 1.2GB 的内存.也可以用BloomFilter
缓存非法 key,每次发现一个 key 是不存在的非法 key,就记录到BloomFilter
中,这种记录方案,会导致BloomFilter
存储的 key 持续高速增长,为了避免记录 key 太多而导致误判率增大,需要定期清零处理. - 布隆过滤器缺点:
- 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中.
- 选择多个 Hash 函数计算多个 Hash 值,这样可以减少误判的几率.
- 不支持删除元素.
- 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中.
- 布隆过滤器测试计算网站: https://krisives.github.io/bloom-calculator/
- 构建一个
缓存雪崩(大量失效)
原因分析
- 缓存雪崩是指⼤量的应⽤请求⽆法在Redis缓存中进⾏处理,紧接着,应⽤将⼤量请求发送到数据库层,导
致数据库层的压⼒激增.-
第⼀个原因是:缓存中有⼤量数据同时过期,导致⼤量请求⽆法得到处理.
- 具体来说,当数据保存在缓存中,并且设置了过期时间时,如果在某⼀个时刻,⼤量数据同时过期,此时,应⽤再访问这些数据的话,就会发⽣缓存缺失。紧接着,应⽤就会把请求发送给数据库,从数据库中读取数据。如果应⽤的并发请求量很⼤,那么数据库的压⼒也就很⼤,这会进⼀步影响到数据库的其他正常业务请求处理。
-
第二个原因是: Redis缓存实例发生故障宕机,无法处理请求.
-
"⼤量数据同时过期"处理方法
-
加互斥锁:跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队.
-
缓存永不过期:设置key永不失效(热点数据);
-
均匀过期:设置key缓存失效时候尽可能错开(把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效);
-
双层缓存策略:比如同时使用redis和memcache缓存,请求->redis->memcache->db;
使用主备两层缓存:
-
主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值.
-
备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存.
-
-
服务降级,是指发⽣缓存雪崩时,针对不同的数据采取不同的处理⽅式。
- 当业务应⽤访问的是⾮核⼼数据(例如电商商品属性)时,暂时停⽌从缓存中查询这些数据,⽽是直接返 回预定义信息、空值或是错误信息;
- 当业务应⽤访问的是核⼼数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续 通过数据库读取。
"Redis实例宕机"处理方法
- 在业务系统中实现服务熔断或请求限流
- 所谓的服务熔断,是指在发⽣缓存雪崩时,为了防⽌引发连锁的数据库雪崩,甚⾄是整个系统的崩溃,我们 暂停业务应⽤对缓存系统的接⼝访问。再具体点说,就是业务应⽤调⽤缓存接⼝时,缓存客⼾端并不把请求 发给Redis缓存实例,⽽是直接返回,等到Redis缓存实例重新恢复服务后,再允许应⽤请求发送到缓存系统。
- 这样⼀来,我们就避免了⼤量请求因缓存缺失,⽽积压到数据库系统,保证了数据库系统的正常运⾏。
缓存数据一致性
原因分析
- 缓存中的数据和数据库中的数据不一致,会导致系统异常
处理办法
- 使用缓存更新策略
- 使用数据同步机制
- 使用读写分离
数据并发竞争
原因分析
- 指多个线程同时对缓存中的数据进行读写操作,会导致缓存数据的不一致性
处理办法
- 使用分布式锁
- 使用版本号控制缓存数据的一致性
缓存击穿Hot Key
原因分析
- 缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大.
处理办法
-
使用互斥锁(mutex key)
-
在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接击中缓存,防止了缓存击穿.
-
热点数据永不过期
-
永不过期实际包含两层意思:
-
物理不过期,针对热点key不设置过期时间
-
逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
-
-
缓存预热
原因分析
- 指在系统启动或高峰期前,预先将热点数据加载到缓存中,以避免请求落到数据库上
处理办法
- 使用缓存预热脚本
- 在系统启动时预加载热点数据
- 使用定时预加载数据
缓存降级
原因分析
- 指高峰期或系统故障时,为了保证系统的可用性,临时关闭缓存服务或将缓存服务降级,以避免由于缓存导致系统崩溃
处理办法
- 使用限流、熔断或降级等手段来保证系统的可用性
总结
问题 | 原因 | 应对方案 |
---|---|---|
缓存雪崩 | 大量数据同时过期 缓存实例宕机 |
给缓存数据的过期时间加上小的随机数据,避免同时过期 服务降级 服务熔断 请求限流 Redis缓存主从集群 |
缓存击穿 | 访问非常频繁的热点数据过期 | 不给热点数据设置过期时间,一直保留 |
缓存穿透 | 缓存和数据库中都没有要访问的数据 | 缓存空值或缺省值 请使用布隆过滤器快速判断 请求入口前端对请求合法性进行检查 |
缓存的读写模式
- 上图来源于https://blog.bytebytego.com/p/top-caching-strategies
Cache Aside
(旁路缓存)
- 更新数据时不更新缓存,而是删除缓存中的数据,在读取数据是,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中.
- 这种策略数据以数据库中的数据为准,缓存中的数据是按需加载的.
- 可以分为读策略和写策略
- 读策略步骤:
- 从缓存中读取数据,若缓存中命中,则直接返回数据;
- 若缓存不命中,则从数据库中查询数据;
- 查询到数据后,将数据写入到缓存中,并且返回给用户.
- 写策略步骤: 先操作数据库,再删除缓存
- 更新数据库中的数据记录
- 删除缓存记录.
- 读策略步骤:
- Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响.如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
- 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了.当然这么做对于写入的性能会有一些影响;
- 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受.
- 适用场景: 读多写少
Read/Write Through
(读写穿透)
- 由系统本身完成,数据库与缓存的问题交由系统本身去处理
Write Behind Caching
(异步缓存写入)
- 调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 HoleLin's Blog!