加载中...

本篇要回答的问题缓存(Cache)到底是不是一种存储中间件?它与存储是什么关系?以及 memcached、groupcache、Redis 这三个模型迥异的缓存系统各自解决了什么问题、又各自不完美在哪?

上一讲收尾了对象存储,至此 KV 存储、数据库、对象存储这几类核心存储中间件已介绍完。本讲聚焦它们的"加速器"——缓存,并借三个代表系统把"缓存是存储的补丁"这个判断讲透。

缓存是什么:存储的加速器

缓存本质是存储的加速器,加速原理通常两种:

加速方式 原理 例子
更高速的硬件 用更快介质挡在慢介质前 SSD 缓存 SATA;内存缓存外存
更短的路径 把复杂计算 y=F(x) 的结果缓存起来 x ⇒ y 的内存缓存避免一连串存储访问

所以缓存的数据结构只需是一个 KV 存储,接口极简:Get / Set / Delete

memcached 与缓存命中率

第一个被广泛应用的内存缓存是 memcached,通常多实例组集群,靠 Hash 或 Range 分片分布数据。典型用法是 FastF(x):先查缓存,miss 则算 F(x) 并回填。

分片方式 优点 缺点
简单 Hash 分片 易理解 扩容时 countOf(memcaches) 变化,大量 key 落到新分片,引发大面积 miss
一致性哈希 扩容时只影响少量 key 实现稍复杂(推荐)

关键判断:缓存核心指标是缓存命中率(Cache Hit Rate)= 命中次数 / 总调用次数。一旦命中率趋势下降并跌破阈值,就该考虑扩容;而 miss 时 FastF 非但不加速,还多了 Get + Set 两次网络请求。

缓存 vs 存储:一个不完美的补丁

缓存"既是也不是"存储中间件:

维度 缓存 一般存储
是否维持业务状态 是(这点像存储)
副本 通常单副本,允许丢数据 多副本,不允许丢
丢数据后果 一般只是后端短时压力变大;极端时雪崩 不可接受

雪崩链条:部分缓存实例宕机 → 命中率下降 → 大量请求压向后端存储 → 后端过载宕机 → 重启又被压垮 → 怎么也起不来。应对:后端存储自带过载保护——并发超预期就主动丢弃部分请求。

关键洞见:缓存其实是存储的"补丁",而且是理论上不完美的补丁

  • 事务(Transaction)改善存储"写操作"的业务匹配性(包装成原子操作);
  • 缓存改善存储"读操作"的匹配性(提升高频读效率)。
    若存储本身足够匹配业务,本不该需要外挂缓存。

为何"不完美"?因为 FastF(x) 没被包装成原子读操作。若 F(x) 会有多个版本的值,并发请求可能拿到不同结果,导致缓存与存储不一致,进而引发依赖该值的业务逻辑错乱。

groupcache:用不可变性消除不一致

memcached 作者 bradfitz 用 Go 写了 groupcache,做了两大改动:

改动 解决的问题
引入 group 概念 同集群缓存多个操作 F(x)、G(x) 时,不必手工拼 F#xG#x;每个 group 独立设内存上限、只淘汰自己组内数据,相当于多个逻辑独立的缓存集群
值不可修改 一旦 x⇒y 就永远是 y,不再需要失效时间,只因内存不足被淘汰,一致性问题彻底消失

关键判断:groupcache 理论完美,但它鼓励用函数式编程实现业务逻辑——而函数式较小众,所以用好它的挑战并不低。

Redis:缓存还是存储?

Redis 定位特别尴尬,有人当缓存、有人当存储。当缓存用时它只需是简单 KV;但它实际是 key ⇒ document(字符串、哈希、列表、集合、有序集合),还支持 Lua 脚本做存储过程,更像数据库。

把 Redis 当存储,有两个非常重要的坑

问题 说明
持久性(Durability) 基于内存,定期刷盘策略不合格(宕机丢上次持久化后的新数据);同机房多副本挡不住机房整体断电,必须多机房多副本才达标——而这实施条件过于苛刻
重试友好性 协议对用户友好但对重试不友好,如 LPUSH 重试可能往列表里塞进多个相同元素

关键判断:Redis 与 memcached 都是"实用型瑞士军刀",很有用,但站在分布式系统理论角度看,都有那么一点不完美

总结

本讲把"缓存"放在存储体系里重新定位:它是存储的加速器、是改善读匹配性的补丁,但因为不是原子读、且通常单副本,所以理论上并不完美。三个代表系统正好构成一条"从实用到理论完美再到模糊定位"的谱系。

先把"是什么"回答清楚

概念 一句话说明
缓存(Cache) 存储的加速器,本质是个 KV,靠更快硬件或更短路径加速
缓存命中率 命中次数 / 总调用次数,缓存系统的核心指标
雪崩 缓存宕→命中率降→后端被压垮→重启又垮的连锁反应
缓存是补丁 改善存储"读操作"匹配性,与事务(改善写)相对
group(groupcache) 独立命名空间 + 独立淘汰,配合"值不可变"消除一致性问题

一句话速记

缓存是存储的"读补丁":memcached 实用但有一致性缺陷,groupcache 用"值不可变"做到理论完美却要你写函数式,Redis 像数据库但持久性与重试友好性两道坎绕不过去。

几条值得记住的判断

  • 缓存允许丢数据、通常单副本——这是它区别于存储的根本点,也是雪崩风险的来源。
  • 缓存不完美的本质是 FastF(x) 不是原子读;值可变就有不一致风险。
  • 凡是想把 Redis 当存储用,多机房多副本是硬门槛,否则机房断电就丢数据。

思考题

groupcache 用"值不可变"换来了完美一致性,代价是逼你走函数式风格。回到你的系统:你前面挂的那层缓存,究竟是在弥补"存储与业务读场景不匹配",还是仅仅在掩盖"存储选型本就不对"?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关