缓存策略简单总结

缓存的三座大山

以下内容摘自:翻越缓存的三座大山

  1. 缓存一致性
    • 缓存一致性是指业务在引入分布式缓存系统后,业务对数据的更新除了要更新存储以外还需要同时更新缓存,对两个系统进行数据更新就要先解决分布式系统中的隔离性和原子性难题。
  2. 缓存击穿
    • 缓存击穿是指查询请求没有在缓存层命中而将查询透传到存储 DB 的问题,当大量的请求发生缓存击穿时,将给存储 DB 带来极大的访问压力,甚至导致 DB 过载拒绝服务。
    • 通过以下方式防止缓存击穿:
      1. 通过 bloomfilter 记录 key 是否存在,从而避免无效 Key 的查询;
      2. 在 Redis 缓存不存在的 Key,从而避免无效 Key 的查询;
  3. 缓存雪崩
    • 缓存雪崩是指由于大量的热数据设置了相同或接近的过期时间,导致缓存在某一时刻密集失效,大量请求全部转发到 DB,或者是某个冷数据瞬间涌入大量访问,这些查询在缓存 MISS 后,并发的将请求透传到 DB,DB 瞬时压力过载从而拒绝服务。目前常见的预防缓存雪崩的解决方案,主要是通过对 key 的 TTL 时间加随机数,打散 key 的淘汰时间来尽量规避,但是不能彻底规避。

本文主要讲的是第一个问题:缓存一致性

基本要点

  1. 缓存替换方式
    • 更新缓存VS淘汰缓存
  2. 缓存替换和写库顺序
    • 先替换缓存后写数据库vs先写数据库后替换
  3. 更新缓存数据源是主还是从
    • 主从数据库下(主从延迟),是从还是主读取数据进行缓存替换
  4. 从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案

Cache-Aside pattern

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

  1. 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  2. 命中:应用程序从cache中取数据,取到后返回。
  3. 更新:先把数据存到数据库中,成功后,再让缓存失效。

大多数业务,使用这样的更新套路即可

解决思路

  1. 以下摘自:高并发下缓存和数据库一致性问题

    • 主从一致性,即修改完立马就要读取到最新的数据(本方案不涉及到缓存的同步,如果涉及可以结合全篇思路去设计) 方案如下:
      1. 半同步复制,理应数据库原生的功能,等从库同步完才返回结果,缺点吞吐量下降
      2. 强制读主库,部分有一致性要求的,代码中强制读取主库,这个时候一定要结合好缓存,提高读性能
      3. 数据库中间件,一般情况数据库中间件把写路由到主,把读路由到从,此处是记录所以写的key,在500ms内读主库,超过500ms后读从库,能保证绝对的一致性,缺点是成本比较高
      4. 缓存记录写key法,发生写操作,把此key记录在缓存里过期时间500ms,key存在表示刚更新过,还没完成同步,强制路由到主库,没有则路由到从库
    • 关于强一致的需求,现实是不多的,本身就使用cache了还要求强一致,貌似本末倒置,但是不排除特殊情况的存在,主要是思路和大家分享。
    • 金钱余额业务有这种强一致需求(用户余额表接近6亿,查询QPS晚高峰4~5k )
  2. 读和写使用分布式锁控制,这样就能保证,先操作(或读或写)数据的先获得结果;写的时候让读流量直接走DB,让更新缓存的操作和写DB的操作串行。

  3. 延时双删策略 (延时减少读到脏数据的概率, 可以异步延时)

    (1)先淘汰缓存
    (2)再写数据库(这两步和原来一样)
    (3)休眠1秒,再次淘汰缓存
    这么做,可以将1秒内所造成的缓存脏数据,再次删除。
    

    延迟删只是减少概率

  4. 只有在读请求比写请求耗时还长的场景下才能产生,实际上这种情况发生的概率会很小

  5. 在读流量走从库的情况下,也有可能会导致缓存不一致。
    由于更新完主库后,binlog还没有同步到从库,这时候DB读到的是旧的值,同样会导致缓存不一致的场景

  6. 缓存更新重试机制:使用MQ或binlog (个人不是很喜欢,除非能很好的抽象成公共组件可以考虑)

  7. 热点数据查主库(同1中的第4点)

    1. 写,更新db,设置热点数据标志(30s(可配置))
  8. 读,判断是否是热点数据。是,直接读主库(主库超过一定qps,读从库),写缓存;否,读缓存

  9. 根据业务id,实时性高的读主库,实时性低的读从库或者缓存

  • 浅谈缓存最终一致性的解决方案
  • 总结: 在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“ Cache-Aside 结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“ Write-Through 结合分布式锁”的方案 ,写多的极端场景下,可以选择采用“ Write-Behind ” 的方案。

其他个人经验

  1. 设置缓存时为了防止穿透,并且具备更新缓存的能力,需要失败时提供默认值,设置较大的过期时间
  2. 那么需要设置:正常数据更新时间R、失败默认数据过期时间DR、数据过期时间E;
  3. 一般E > R > DR
  4. 并且,当缓存已经有数据时, 重新远程获取数据失败时,不应该更新缓存

扩展

  • 太强了,全面解析缓存应用经典问题
    • 缓存的主要存储模式
      1. Cache Aside(旁路缓存)
      2. Read/Write Through(读写穿透)
      3. Write Behind Caching(异步缓存写入)
    • 缓存7大经典问题的常用解决方案
      1. 缓存集中失效
      2. 缓存穿透
      3. 缓存雪崩
      4. 缓存数据不一致
      5. 竞争并发
      6. 热点Key问题
      7. 大Key问题

Reference

  1. 高并发下缓存和数据库一致性问题(更新淘汰缓存不得不注意的细节)
  2. 分布式之数据库和缓存双写一致性方案解析
  3. 翻越缓存的三座大山
  4. 解析分布式系统的缓存设计