再快也有一次往返
Redis 已经把查库的十毫秒压到一毫秒以内,为什么还要更快?看一组数:同机房 Redis 一次网络往返约 0.5~1ms,加上序列化、连接池排队,一次缓存读的实际成本在毫秒级;而进程内存里的本地缓存是纳秒到微秒级,差着三个数量级。万级 QPS 的服务里,毫秒与微秒的差就是 CPU 与线程池的真金白银。热 Key 篇已经埋了伏笔:本地缓存扛第一波,本篇把这个思路补完——L1 本地缓存(Caffeine/Guava)+ L2 分布式缓存(Redis)+ 数据库的三级读路径。
读路径:逐级往下找
完整的多级读链路是一条漏斗:请求先查 L1,命中直接返回(纳秒级,扛住绝大多数热读);未命中查 L2 Redis,命中后回填 L1再返回;再未命中才回源 DB,结果同时灌进 L2 和 L1。每往下一级,成本涨一个量级,所以漏斗的设计目标就是让流量尽量停在最上层:
Cache<String, String> l1 = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(10)) // L1: 秒级短TTL
.recordStats()
.build();
public String get(String key) {
String v = l1.getIfPresent(key); // L1 命中:纳秒级
if (v != null) return v;
v = redis.get(key); // L2 命中:亚毫秒级
if (v != null) { l1.put(key, v); return v; }
v = loadFromDb(key); // 回源:毫秒级
redis.setex(key, ttl(key), v);
l1.put(key, v);
return v;
}注意 L1 的 TTL 是秒级的,远短于 L2——这是有意为之:本地副本存活越久,脏得越久,短 TTL 是控制脏窗口的第一道闸。
真正的难题:多实例一起失效
单实例的失效好办,多级缓存的麻烦在于:数据变了,几十个应用实例各自的 L1 都得知道。Redis 是共享的,删一次全体生效;Caffeine 是进程私有的,删自己的不删别人的。主流解法是失效广播:数据变更方把「删 key」的事件发到 Redis Pub/Sub 或 MQ,所有实例订阅,收到就清掉自己 L1 里对应的 key;广播丢了也没关系,L1 的秒级 TTL 兜底,脏窗口封顶十秒:
变更方:update db ──> del redis ──> publish "evict:item:10086"
各实例:subscribe 收到消息 ──> caffeine.invalidate(key)延迟敏感的场景可以把广播换成 binlog 订阅触发(第 7 篇的链路直接复用),变更方代码一行不加。
边界:不是什么数据都配进 L1
多级缓存是重装备,准入门槛要守住:极热——单 key QPS 数千以上才值得(热 Key 篇的判定标准);读多写少——数据频繁变,L1 就是脏数据分发的帮凶;容量可控——本地缓存吃的是应用进程的堆内存,maximumSize 必须设。典型适合的是商品详情、配置、白名单、词典数据;典型不适合的是库存、余额这类强一致数据——它们的问题域在一致性篇,不在性能篇。
多级架构立起来之后,评价它好不好只有一个数字说了算:命中率。下一讲把这个缓存体系最重要的指标讲透。
评论 (0)