本篇要回答的问题:缓存(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#x、G#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 用"值不可变"换来了完美一致性,代价是逼你走函数式风格。回到你的系统:你前面挂的那层缓存,究竟是在弥补"存储与业务读场景不匹配",还是仅仅在掩盖"存储选型本就不对"?
