1-介绍

缓存系统的设计 过于复杂,这里说一下通用的问题

  • Backbone 组件选型, 是 caffeineLocalCache , 还是 Redis, Memcached 的集中式缓存
  • 数据结构的选择: 缓存可以是各种数据结构, String, Hash, SkipList, BloomFilter 这些简单的,还是 更复杂的 d-ary-heap , tries 树, Hnsw, 就以 Redis 为例, 他的社区 应该都支持
  • 序列化方式: json or protoBuf ..
  • Sharding 实现的方式, 根据 Id Hash 还是 Range
  • 分片规则是放在 Client, 还是做一个 Cache Gateway, 还是使用 Sidecar 的模式管理规则
  • 读写模式是什么, 例如是不是简单的 Cache, 还是需要 跳表分页 ?
  • 缓存的位置, 要几层
  • 缓存的过期策略, 直接使用 ttl 过期还是 妖路 比如Key 带上过期时间等
  • 缓存的常见问题如何提前解决: 穿透,失效,雪崩

2-模式

大多数的模式都是包装了 酷壳-缓存更新的4种模式 的更新套路.

  • Cache Aside : 调用方同时 关注 CacheDB 组件 , 自己控制 什么时候更新 Cache, 什么时候更新 DB ;
  • Read/Write Through : 把 Cache 和 DB 当成一个 组件, 在 Miss 的时候或者 修改的时候去触发更新 ;
  • Write Behind Caching : LinuxPageCache 更新算法 ;

这里 解释一些误区:

  1. 先更新 DB, 再更新 Cache 并不能保证一致性(其实不管怎么样,都不能保证), 只是 这种设计, 更容易去实现 补偿和最终一致性, 简化了 实现最终一致性的成本,你只要认为 DB 一定对就行了 ;
  2. 更新 Cache 的时候是 清空还是修改操作, 修改应该可以优化读取的性能,但是清空对内存的利用率会更友好一些, 个人觉得 大多数场景下清空更划算 ;

3-穿透

描述: 这个 Key 本身在 DB 中就不存在. 导致 findByKey() 在 Cache 中一直 Miss, 一直穿透到 DB

  • 量小可以不管, 量大可以用 空间换时间 的基本思路

例如操作系统 Linux 中的 nscd 增加了一个 negative-time-to-live 的配置来解决这个问题.

$ cat /etc/nscd.conf 
enable-cache            hosts           yes
positive-time-to-live   hosts           3600
negative-time-to-live   hosts           20
suggested-size          hosts           211
check-files             hosts           yes
persistent              hosts           yes
shared                  hosts           yes
max-db-size             hosts           33554432
  • 负向行为的数据可以考虑使用单独的数据结构,因为它没有数据, 可以用 Hash 或者 BloomFilter

4-失效

描述: 大量的 Key 同时失效, 导致同时回源到 DB, 仿佛 Cache 失效了

这种现在很容易出现,比如说 由于缓存组件重新启动了,我们来了一个非常正常的 预热操作, 这一大批数据是同时上线的,如果不注意,就会有很多的数据同时 失效.

一个简单的方案是 过期时间增加随机性, 例如 ttl = base + random

有的时候对于单个 Key, 如果是 hotKey, 失效的问题也很大

  • 这个时候本质上是并发的问题, 对于单个 Key 可以控制 refreshHotKey 的并行粒度
    • 可以应用内加锁
    • 可以使用 lua 跨应用加锁

5-雪崩

描述: 是指由于 部分的缓存挂掉之后, 故障蔓延 到整个缓存系统,再到整个 Db, 然后就全站挂了

这个问题的可能原因比较多, 先说一些通用的 缓解思路:

  1. 熔断: 熔断是 解决所有故障蔓延 的通用思路, 如果发现大量的 CacheException , failfast 直接失败掉,然后返回降级结果, 可以有效防止蔓延

  2. 使用限流保护 DB, 这个是兜底的, 更缓存组件设计无关

  3. 使用影子集群, 使用廉价的, 通用的集群组件去做自动切换, 临时顶一下.

Facebook 开源的 mcrounter + memcached协议兼容组件 + k8s-sidecar 在解决这些问题上有突出的表现,特别是 雪崩问题.

雪崩的问题 核心还是在于预防.

  1. 高并发的缓存组件, 可以用 k8s 弹性伸缩一下, 往往都是 由于 没有预料的流量洪峰造成的
  2. 缓存组件异地多活, 增加组件本身的 跨数据中心容错能力
  3. 监控,报警,要能够提前发现问题

Tips

这里强调 Memcached 协议的兼容组件, 因为要支持Memcached 协议是非常简单的, 可以考虑用一些廉价的组件,甚至是自研的方式提供 Backup, 比如 Mysql, Pg 都支持 Memcached 协议, 高性能的比如 Dragonfly 也完全支持 这个协议

6-一致性

首先,客观理解缓存的一致性问题

首先缓存,本质上是一个 Slave , 是基于 密集型原理 用来提升 读性能的组件, 是个 分布式的组件.

应该追求 的是 最终一致性 而不是 强一致性 .也就是缓存,必然要容忍一段时间(比如说 seconds) 的不一致, 哪怕 Mysql 的从库也会有这个问题.

不一致的本质问题基本都是处理写入

  1. 一定要优先改 Db, 再去修改 Cache, 这样比较好 补偿实现最终一致 ;
  2. 缓存 仅仅是缓存, 如果你要把他同时作其他的功能, 例如 Counter, 那是另外的问题 ;
  3. 修改的时候, 清空 Cache 比直接修改会有更少的 写冲突造成的长时间不一致问题, 但是性能会 有轻微的损失 ;
  4. 更新的套路有很多,比如说不走应用层面的修改, 走数据层面的 cdc 会有更高的一致性, 例如 ScyllaDb cdc, Netflix Dblog - CDC 等等架构, Mysqlcdc 更是常态, 由于 redis, mem 缓存的更新操作, 不管是 set 还是 clean 都是幂等操作, 配合 flink 能非常轻易的实现 Exactly once 级别的一致性,不会比 Mysql 的主从同步差

有的时候 不一致问题 还可能是由于 分布式的问题

例如一致性 HashRehash vnode 的时候, 这个时候中间也有会 过程中不一致的情况, 这种情况不能忍就换 一致性协议 或者 走更严谨的 Rehash 策略 .

7-HotKey

描述: 缓存的基本是 密集型原理,密集型原理有的时候也是一把双刃剑.

数据分冷热,有的数据特别热就会是 HotKey, 例如某个 超级流量明星 , 微博因此挂掉也不是 一两次.

再由于大多数的 缓存都是 Hash 的, 导致大部分的流量 打到了一台服务器上 .

解决: 这本质上是个业务问题, 从缓存这一侧看 无法提前预知到 哪个 KeyhotKey, 自然不会特殊处理给最大的资源

从缓存侧解决的思路是, 先发现 再报警 再处理 ,因此 只能是比较 滞后的处理, 而且是比较通用的处理,由于不能动 业务的 Key 规则, 只能动态的增加资源等等

这个问题的核心是 要 尽量提前发现这样的 key, 例如某个可能的大V, 某些活动,推广等等, 应该是能提前分析出来的.

这个时候做的就是对于 HotKey 特殊策略了:

  1. 例如对这个 Key 的进一步拆分
  2. 例如对这个 Key 的多级缓存,更多的分层处理等等
  3. 例如对这个 Key 更给力的硬件
  4. 这些 Key 入由到更细的分片规则,例如一个 Key 一个 Node

建议的是优先提前发现,缓存侧提供 HotKey 的后置报警

可以参考 阿里云寻找并处理 hotKey 和 largeKey

8-BigKey

描述: 同上,只是关注点在于 数据倾斜上. 解决思路也类似

Refer