10-分布式缓存

缓存

常见的缓存更新策略

Cache Aside Pattern(旁路缓存模式)

  • 应用直接管理缓存,缓存层独立于数据库
  • 比较适合读请求比较多的场景

  • 先更新DB
  • 直接删除cache

  • 先从cache中读取数据,读取到就直接返回
  • cache读取不到的话,就从DB中读取返回
  • 再把DB中读取到的数据放到cache中

写操作为什么是删除cache而不是更新cache?

  • 对服务端资源造成浪费,因为cache中的数据可能需要经过服务端大量的计算才能拿到,对于服务端来说更新cache是一笔开销,如果频繁修改DB,而需要频繁计算,但是并不能保证计算出来的cache一定被使用,造成资源的浪费
    • 举例: cache A: 需要数据 DATA B 和 DATA C 计算得出,DATA B 每天一个时间段内更新一万次,但是cacha A 每天均匀的读取100次,
  • 产生数据不一致的问题,并发场景下,更新cache产生数据不一致的概率会更大。

写操作为什么不先删除cache在更新DB?

  • 会造成数据库和缓存不一致的情况
  • 先写DB在删除cache不会出现数据不一致的问题嘛?
    • 会的,但是因为写入缓存的速度比写入DB的速度快很多,所以概率比较小

缺陷

  • 首次请求数据一定不在cache中的问题,缓存穿透的问题
    • 缓存热点数据
  • 写操作比较频繁的话导致cache中删除的频率也比较频繁,会影响缓存的命中率。
    • 缓存和数据库强一致的场景:更新DB的同时更新cache,加锁保证cache更新时不会出现线程安全问题
    • 可以短暂的允许不一致的场景:更新DB的同时更新cache,给缓存加一个较短的过期时间,

强一致性的保证

  • 查询操作: 当key不存在时, 查询DB和更新缓存 两个操作加锁
  • 写操作: 写DB 和 删除缓存 两个操作加锁
  • 如果锁存在则直接查询DB

Read/Write Through Pattern(读写穿透)

  • 把Cache视为主要数据存储,从中读取数据并将数据写入其中,cache服务负责将此数据读取和写入DB中

  • 先查cache,cache中不存在,直接更新DB

  • cache中存在,先更新cache,然后由cache服务自己更新DB

  • image-20240225215037444

  • 从cache中读取数据,读取到就直接返回
  • 读取不到的话,先从DB加载,写入到cache后返回响应(高并发下数据不一致问题? 加锁和版本号)

Write Behind pattern (异步缓存写入)

  • 和Read/Write Through Pattern 很相似,都是cache服务来负责cache和DB的读写
  • 不同的是 Read/Write Through Pattern 是同步更新cache和db,而Write Behind 则是只更新缓存,不直接更新DB,而是改为异步批量的方式来更新DB
  • 应用场景
    • 消息队列中消息的异步写入磁盘
    • MySql的Innodb buffer Pool机制
  • 写性能非常高,非常适合数据经常变化但是对数据一致性要求没那么高的场景

Refresh-Ahead(预刷新)

  • 在缓存过期前自动刷新数据。

基于事件的更新(如 Binlog + 消息队列)

  • 通过数据库变更事件触发缓存更新。

选择策略的考量因素

  1. 一致性需求:强一致性选 Write-Through,最终一致性可选 Write-Behind。
  2. 性能要求:高并发写入场景适合 Write-Behind。
  3. 实现复杂度:Cache-Aside 最简单,Write-Behind 最复杂。
  4. 数据冷热:热点数据适合预加载(Refresh-Ahead)。

缓存属性

  • 吞吐量
  • 命中率
  • 扩展功能
  • 分布式支持

缓存算法

  • FIFO
  • LRU
    • 优先淘汰最久未被使用访问过的数据。
  • LFU
    • 优先淘汰最不经常使用的数据
  • Tiny LFU
    • 为了缓解 LFU 每次访问都要修改计数器所带来的性能负担,inyLFU 会首先采用 Sketch 对访问数据进行分析,所谓 Sketch 是统计学上的概念,指用少量的样本数据来估计全体数据的特征,这种做法显然牺牲了一定程度的准确性,但是只要样本数据与全体数据具有相同的概率分布,Sketch 得出的结论仍不失为一种高效与准确之间权衡的有效结论。借助Count–Min Sketch算法(可视为布隆过滤器的一种等价变种结构),TinyLFU 可以用相对小得多的记录频率和空间来近似地找出缓存中的低价值数据。为了解决 LFU 不便于处理随时间变化的热度变化问题,TinyLFU 采用了基于“滑动时间窗”(在“流量控制”中我们会更详细地分析这种算法)的热度衰减算法,简单理解就是每隔一段时间,便会把计数器的数值减半,以此解决“旧热点”数据难以清除的问题。
  • W-Tiny LFU
    • 为了改善Tiny LFU无法很好应对稀疏突发访问的问题,所谓稀疏突发访问是指由一些绝对频率较小但是突发访问频率很高的数据。譬如某些运维性质的任务,也许一天、一周只会在特定时间运行一次,其余时间都不会用到,此时 TinyLFU 就很难让这类元素通过 Sketch 的过滤,因为它们无法在运行期间积累到足够高的频率。
    • 应对短时间的突发访问是 LRU 的强项,具体做法是将新记录暂时放入一个名为 Window Cache 的前端 LRU 缓存里面,让这些对象可以在 Window Cache 中累积热度,如果能通过 TinyLFU 的过滤器,再进入名为 Main Cache 的主缓存中存储,主缓存根据数据的访问频繁程度分为不同的段(LFU 策略,实际上 W-TinyLFU 只分了两段),但单独某一段局部来看又是基于 LRU 策略去实现的(称为 Segmented LRU)。每当前一段缓存满了之后,会将低价值数据淘汰到后一段中去存储,直至最后一段也满了之后,该数据就彻底清理出缓存。TODO:原理

分布式缓存

  • 复制式缓存
    • 复制式缓存可以看作是“能够支持分布式的进程内缓存”,它的工作原理与 Session 复制类似。缓存中所有数据在分布式集群的每个节点里面都存在有一份副本,读取数据时无须网络访问,直接从当前节点的进程内存中返回,理论上可以做到与进程内缓存一样高的读取性能;当数据发生变化时,就必须遵循复制协议,将变更同步到集群的每个节点中,复制性能随着节点的增加呈现平方级下降,变更数据的代价十分高昂。
  • 集中式缓存
    • 分布式缓存的主流形式
    • 集中式缓存的读、写都需要网络访问,其好处是不会随着集群节点数量的增加而产生额外的负担,其坏处自然是读、写都不再可能达到进程内缓存那样的高性能
    • 它与使用缓存的应用分处在独立的进程空间中,这么做的好处是能够为异构语言提供服务,达成语言解耦的目的。坏处是如果要缓存对象等复杂类型的话,基本上就只能靠序列化来支撑具体语言的类型系统(支持 Hash 类型的缓存,可以部分模拟对象类型)不仅有序列化的成本,还很容易导致传输成本也显著增加。

分布式缓存一致性

  • 保证缓存一致性的设计模式:Cache Aside、Read/Write Through、Write Behind Caching
  • 保证缓存一致性的基本手段:定时任务扫描,业务系统MQ,binlog变更MQ
  • 缓存更新时,为了防止产生脏数据,需要加锁,但是还不能保证当前更新的数据就是最新的数据,需要比较数据的产生时间。
  • 对于时间相同的数据,更新时发现时同一秒的数据,但是值不相等,通过消息触发二次更新来保证缓存中的数据是最新的

优秀文章


10-分布式缓存
https://x-leonidas.github.io/2022/02/09/12分布式系统/10-分布式缓存/
作者
听风
发布于
2022年2月9日
更新于
2025年5月12日
许可协议