1-介绍
缓存系统的设计 过于复杂,这里说一下通用的问题
Backbone组件选型, 是 caffeine 的LocalCache, 还是Redis,Memcached的集中式缓存- 数据结构的选择: 缓存可以是各种数据结构,
String,Hash,SkipList,BloomFilter这些简单的,还是 更复杂的d-ary-heap,tries树,Hnsw, 就以Redis为例, 他的社区 应该都支持 - 序列化方式:
jsonorprotoBuf.. Sharding实现的方式, 根据IdHash还是Range- 分片规则是放在
Client, 还是做一个Cache Gateway, 还是使用Sidecar的模式管理规则 - 读写模式是什么, 例如是不是简单的
Cache, 还是需要 跳表分页 ? - 缓存的位置, 要几层
- 缓存的过期策略, 直接使用
ttl过期还是 妖路 比如Key带上过期时间等 - 缓存的常见问题如何提前解决: 穿透,失效,雪崩
2-模式
大多数的模式都是包装了 酷壳-缓存更新的4种模式 的更新套路.
Cache Aside: 调用方同时 关注Cache和DB组件 , 自己控制 什么时候更新Cache, 什么时候更新DB;Read/Write Through: 把Cache 和 DB当成一个 组件, 在Miss的时候或者 修改的时候去触发更新 ;Write Behind Caching:Linux的PageCache更新算法 ;
这里 解释一些误区:
- 先更新
DB, 再更新Cache并不能保证一致性(其实不管怎么样,都不能保证), 只是 这种设计, 更容易去实现 补偿和最终一致性, 简化了 实现最终一致性的成本,你只要认为 DB 一定对就行了 ; - 更新
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, 然后就全站挂了
这个问题的可能原因比较多, 先说一些通用的 缓解思路:
-
熔断: 熔断是 解决所有故障蔓延 的通用思路, 如果发现大量的
CacheException,failfast直接失败掉,然后返回降级结果, 可以有效防止蔓延 -
使用限流保护
DB, 这个是兜底的, 更缓存组件设计无关 -
使用影子集群, 使用廉价的, 通用的集群组件去做自动切换, 临时顶一下.
Facebook 开源的 mcrounter + memcached协议兼容组件 + k8s-sidecar 在解决这些问题上有突出的表现,特别是 雪崩问题.
雪崩的问题 核心还是在于预防.
- 高并发的缓存组件, 可以用
k8s弹性伸缩一下, 往往都是 由于 没有预料的流量洪峰造成的 - 缓存组件异地多活, 增加组件本身的 跨数据中心容错能力
- 监控,报警,要能够提前发现问题
Tips
这里强调
Memcached协议的兼容组件, 因为要支持Memcached协议是非常简单的, 可以考虑用一些廉价的组件,甚至是自研的方式提供Backup, 比如Mysql,Pg都支持Memcached协议, 高性能的比如Dragonfly也完全支持 这个协议
6-一致性
首先,客观理解缓存的一致性问题
首先缓存,本质上是一个 Slave , 是基于 密集型原理 用来提升 读性能的组件, 是个 分布式的组件.
应该追求 的是 最终一致性 而不是 强一致性 .也就是缓存,必然要容忍一段时间(比如说 seconds) 的不一致, 哪怕 Mysql 的从库也会有这个问题.
不一致的本质问题基本都是处理写入
- 一定要优先改
Db, 再去修改Cache, 这样比较好 补偿实现最终一致 ; - 缓存 仅仅是缓存, 如果你要把他同时作其他的功能, 例如
Counter, 那是另外的问题 ; - 修改的时候, 清空
Cache比直接修改会有更少的 写冲突造成的长时间不一致问题, 但是性能会 有轻微的损失 ; - 更新的套路有很多,比如说不走应用层面的修改, 走数据层面的
cdc会有更高的一致性, 例如 ScyllaDb cdc, Netflix Dblog - CDC 等等架构,Mysql的cdc更是常态, 由于redis,mem缓存的更新操作, 不管是set还是clean都是幂等操作, 配合flink能非常轻易的实现Exactly once级别的一致性,不会比Mysql的主从同步差
有的时候 不一致问题 还可能是由于 分布式的问题
例如一致性 Hash 在 Rehash vnode 的时候, 这个时候中间也有会 过程中不一致的情况, 这种情况不能忍就换 一致性协议 或者 走更严谨的 Rehash 策略 .
7-HotKey
描述: 缓存的基本是 密集型原理,密集型原理有的时候也是一把双刃剑.
数据分冷热,有的数据特别热就会是 HotKey, 例如某个 超级流量明星 , 微博因此挂掉也不是 一两次.
再由于大多数的 缓存都是 Hash 的, 导致大部分的流量 打到了一台服务器上 .
解决: 这本质上是个业务问题, 从缓存这一侧看 无法提前预知到 哪个
Key是hotKey, 自然不会特殊处理给最大的资源
从缓存侧解决的思路是, 先发现 → 再报警 → 再处理 ,因此 只能是比较 滞后的处理, 而且是比较通用的处理,由于不能动 业务的 Key 规则, 只能动态的增加资源等等
这个问题的核心是 要 尽量提前发现这样的 key, 例如某个可能的大V, 某些活动,推广等等, 应该是能提前分析出来的.
这个时候做的就是对于 HotKey 特殊策略了:
- 例如对这个
Key的进一步拆分 - 例如对这个
Key的多级缓存,更多的分层处理等等 - 例如对这个
Key更给力的硬件 - 这些
Key入由到更细的分片规则,例如一个Key一个Node - …
建议的是优先提前发现,缓存侧提供 HotKey 的后置报警
可以参考 阿里云寻找并处理 hotKey 和 largeKey
8-BigKey
描述: 同上,只是关注点在于 数据倾斜上. 解决思路也类似