一、服务端缓存 vs 客户端缓存
1.1 延迟的构成
一次 Redis 访问的耗时由三部分构成:客户端处理时间 + 网络往返(RTT)+ 服务端处理时间。其中服务端处理通常只有几十微秒,而网络 RTT 在数据中心内也有 0.1~1ms,跨地域可达几十毫秒。当业务热数据被高频读取时,每次全走网络是巨大浪费。
# 单次 GET 各阶段耗时示意
# 客户端序列化 ~10us + 网络 RTT ~500us(同机房) + 服务端处理 ~50us
# 合计约 560us,其中网络占主导
1.2 两层缓存的定位
| 对比项 | 服务端缓存(Redis 本身) | 客户端缓存(本地) |
|---|---|---|
| 存储位置 | Redis 节点内存 | 应用进程内存 |
| 访问路径 | 走网络 + 协议 | 纯本地内存 |
| 典型延迟 | 0.1~1ms | <10us |
| 容量 | 受限于 Redis maxmemory | 受限于应用堆 |
| 数据一致性 | 天然一致 | 需要失效机制 |
1.3 何时需要客户端缓存
客户端缓存适用于读多写少、value 不大、热点集中的数据:
# 适合:用户画像、配置项、字典表、首页热点数据
# 不适合:频繁更新、大 value、一次性读取的数据
客户端缓存本质是「用一致性复杂度换延迟」。Redis 6.0 的 CLIENT TRACKING 用服务端推送失效事件,让本地缓存无需轮询也能保持新鲜,把复杂度降到最低。
二、RESP2/RESP3 协议
2.1 协议演进
RESP(REdis Serialization Protocol)是客户端与服务端的通信协议。RESP2 时代只能返回「数组 + 简单字符串」,无法表达更多数据类型;RESP3 引入推送(push)消息,成为客户端缓存失效通知的载体。
| 特性 | RESP2 | RESP3 |
|---|---|---|
| 批量字符串 | $3\r\nfoo | $3\r\nfoo |
| 数组 | *2 ... | *2 ... |
| Map/Set/Boolean | 不支持 | 原生类型 |
| 推送消息 | 不支持 | > 前缀,客户端缓存必需 |
| 空值 | $-1(nil) | _ |
# 启用 RESP3(redis-cli 6.0+ 默认)
redis-cli -3 -p 6379
# 或通过 HELLO 协商
HELLO 3
# 1# "server" => "redis"
# 3# "proto" => 3
2.2 HELLO 命令
RESP3 不是默认启用,客户端需通过 HELLO 主动协商:
HELLO 3 AUTH default mypassword # 协商协议并认证
老客户端(RESP2)无法接收推送消息。开启 CLIENT TRACKING 必须使用 RESP3 连接,否则会报错。升级客户端库时注意库的 RESP3 支持版本。
三、CLIENT TRACKING 失效推送
3.1 工作原理
客户端开启 tracking 后,Redis 会记录该客户端读过哪些 key。当这些 key 被修改或删除时,Redis 通过**失效推送消息(invalidation message)**主动通知客户端:本地缓存里对应的 key 可以作废了。
# 客户端 A 开启 tracking(RESP3 连接)
CLIENT TRACKING ON
GET hotkey # 读取即被追踪
# 客户端 B 修改 hotkey
SET hotkey new_value
# 客户端 A 立即收到推送(invalidation 事件)
# >2 / >6 invalidation / >3 hotkey
3.2 追踪粒度与成本
Redis 用**全局失效表(invalidation table)**记录「key → 哪些客户端缓存了它」。每个被追踪的 key 都会占内存,因此追踪的 key 不宜过多:
CLIENT TRACKING INFO
# tracking:on
# tracking_redirect:0
| 指标 | 影响 |
|---|---|
| 追踪 key 数量 | 失效表内存占用 |
| 失效频率 | 推送消息带宽 |
失效表并非无限增长:服务端对每个客户端有数量限制,超过后可能只推送 key 名列表而不逐 key 追踪(bcast 模式的一种退化)。生产要控制追踪规模。
3.3 失效推送的可靠性
推送基于长连接,若客户端断线期间 key 变更,断线期间的失效事件会丢失。因此:
# 重连后必须丢弃本地缓存或做一次全量失效
CLIENT TRACKING ON RESET # 重连时先 RESET 再 ON
客户端缓存的一致性边界:推送保证「连接存活期间的强一致」,断线窗口只能靠 TTL 兜底。这是客户端缓存的固有权衡。
四、缓存模式(default/opt-in/bcast/redirect)
4.1 模式总览
CLIENT TRACKING 提供四种模式,按「追踪范围」和「推送方向」区分:
| 模式 | 语法 | 追踪范围 | 适用 |
|---|---|---|---|
| default | CLIENT TRACKING ON | 所有读过的 key 自动追踪 | 通用场景 |
| opt-in | ON OPTIN + CLIENT CACHING YES | 仅显式标记的 key | 精准控制 |
| opt-out | ON OPTOUT + CLIENT CACHING NO | 除标记外全部 | 少量排除 |
| bcast | ON BCAST PREFIX user: | 按前缀广播失效 | 前缀型热数据 |
4.2 default 模式
CLIENT TRACKING ON
GET user:1001 # 自动追踪 user:1001
# 该 key 被修改时推送失效
优点:零配置;缺点:失效表增长不可控。
4.3 opt-in / opt-out 模式
# opt-in:只有显式标记的 key 才追踪
CLIENT TRACKING ON OPTIN
CLIENT CACHING YES
GET user:1001 # 仅这个被追踪
# opt-out:默认全追踪,显式排除
CLIENT TRACKING ON OPTOUT
CLIENT CACHING NO
GET user:1001 # 不追踪
4.4 bcast 广播模式
bcast 模式下,客户端不再逐个上报读取的 key,服务端按前缀广播所有失效事件,客户端本地再过滤:
CLIENT TRACKING ON BCAST PREFIX user: PREFIX config:
# 以 user: / config: 开头的 key 变更都推送过来
# 优点:无需维护失效表,内存零开销
# 缺点:广播风暴——不相关的 key 变更也会推送
# 适用:前缀规整、前缀下 key 总量可控的场景
4.5 redirect 重定向模式
当客户端连接由代理(如集群客户端、连接池)管理时,tracking 无法直接建立。redirect 让一个连接专门接收失效推送,另一个连接读数据:
# 连接 A(id=5):专门接收推送
CLIENT TRACKING ON REDIRECT 5
# 连接 B:正常读写,其失效由连接 A 汇聚接收
GET hotkey
redirect 是集群/连接池场景的救星:业务连接不需要全部开 tracking,一个专职推送连接即可服务一批缓存客户端。
五、TTL 与失效处理
5.1 双重保险
推送失效是主要机制,TTL 是兜底。两者配合形成「准实时失效 + 最终过期」的完整防线:
# 本地缓存设计三重保障
# 1. 收到 invalidation → 立即删除本地 key
# 2. 本地缓存自带 TTL(如 30~60s)→ 兜底断线窗口
# 3. 冷数据按 TTL 自然淘汰
| 场景 | 推送失效 | TTL 兜底 |
|---|---|---|
| 连接正常 | 生效 | 冗余但无害 |
| 连接断开 | 丢失 | 唯一保障 |
| 未追踪的 key | 无 | 唯一保障 |
5.2 失效处理实现
// Java 示例(伪代码):收到 invalidation 后立即失效,下次回源
Map<String, CacheEntry> localCache;
void onInvalidation(String key) { localCache.remove(key); }
5.3 TTL 选取
# TTL 过长 → 断线窗口内脏数据越久;过短 → 命中率下降
# 经验值:热点数据 30~60s;可容忍短暂脏读的 5~10 分钟
TTL 不是「越小越一致」:太小会让客户端缓存失去意义。要结合业务对脏数据的容忍度选择,一致性要求高的场景甚至应禁用本地缓存。
六、多语言客户端支持
6.1 生态支持矩阵
| 语言/客户端 | 版本 | RESP3 | CLIENT TRACKING |
|---|---|---|---|
| Java Lettuce | 6.1+ | 完整 | 完整(失效订阅) |
| Java Jedis | 4.x | 部分 | 需手动处理 |
| Go go-redis/v9 | v9.1+ | 完整 | 完整 |
| Python redis-py | 4.2+ | 完整 | 完整 |
| Node ioredis | 5.x | 完整 | 完整 |
6.2 go-redis 示例
// go-redis/v9 开启客户端缓存
client := redis.NewClient(&redis.Options{Addr: "localhost:6379", Protocol: 3})
client.Do(ctx, redis.NewCmd("CLIENT", "TRACKING", "ON"))
// 库层通过 __redis__:invalidate 接收推送并回调清理本地缓存
6.3 注意事项
# 1. 连接必须保持长连接(推送依赖它)
# 2. 连接池中每个连接都要各自开启 tracking
# 3. 断线重连后重新开启并全量失效
# 4. 集群/哨兵模式下用 redirect 汇聚推送
多语言客户端对 tracking 的封装深度不一。实现前先确认所选客户端库是否提供「失效推送回调」能力,否则需要自行订阅
__redis__:invalidate频道(实际由库层完成)。
七、一致性权衡
7.1 一致性模型
| 方案 | 一致性 | 延迟 | 复杂度 |
|---|---|---|---|
| 仅 Redis | 强一致 | 高(每次走网络) | 低 |
| 客户端缓存 + tracking | 准实时(断线窗口弱一致) | 低 | 中 |
| 客户端缓存 + 轮询 | 取决于轮询间隔 | 低 | 低 |
| 客户端缓存 + 短 TTL | 最终一致 | 低 | 低 |
7.2 失效延迟的构成
# 写 key → 本地失效 的延迟 ≈ 服务端处理 + 推送 RTT + 回调处理 ≈ 1~3ms
# 可视为准实时
7.3 权衡决策
| 业务场景 | 建议 |
|---|---|
| 数据 5 分钟不更新也行 | 纯 TTL,无需 tracking |
| 需要秒级一致 | tracking + 短 TTL |
| 强一致(金钱、库存) | 不用客户端缓存 |
决策铁律:先问「脏读容忍度」,再选方案。强一致场景永远不要引入本地缓存;能接受秒级弱一致才考虑 tracking。
八、本地缓存实现
8.1 缓存数据结构
// 本地缓存容器:并发安全 + 兜底 TTL
class LocalCache<K, V> {
ConcurrentHashMap<K, CacheEntry<V>> map; // 并发安全
long ttlMillis; // 兜底 TTL
}
8.2 读路径与写路径
// 读:本地命中直接返回;未命中回源 Redis 并写本地
V get(K key) {
CacheEntry<V> entry = map.get(key);
if (entry != null && !entry.expired()) return entry.value;
V value = redis.get(key); // 回源
map.put(key, new CacheEntry<>(value, now + ttlMillis));
return value;
}
void onInvalidation(K key) { map.remove(key); }
8.3 内存与淘汰
# 本地缓存必须设容量上限(如单 JVM < 256MB)
# 淘汰策略:LRU/LFU + TTL 双维度
本地缓存放在应用进程内,内存要预算。最稳妥的做法是「容量上限 + 每 key TTL + LRU 淘汰」,三者缺一不可。
九、最佳实践
9.1 落地清单
| 步骤 | 内容 |
|---|---|
| 1 | 评估热点数据与脏读容忍度 |
| 2 | 客户端升级到 RESP3 支持版本 |
| 3 | 选择 tracking 模式(default/opt-in/bcast/redirect) |
| 4 | 本地缓存设置容量上限与 TTL |
| 5 | 实现失效推送回调 |
| 6 | 断线重连时全量失效 |
| 7 | 压测对比 P99 延迟收益 |
9.2 监控指标
# 关注三类指标:命中率(目标>80%)、回源 QPS(下降即验证)、推送延迟
# 压测对比示例
# 优化前 P99 GET 延迟: 1.2ms → 优化后: 0.05ms(本地命中),命中率 92%
9.3 常见误区
| 误区 | 正确做法 |
|---|---|
| 所有数据都本地缓存 | 只缓存热点小数据 |
| 不设 TTL | TTL 是断线窗口唯一保障 |
| 忽略连接池 | 每个连接单独开 tracking |
| 集群直接用 default | 优先 bcast + prefix 或 redirect |
| 不做压测 | 用命中率与 P99 验证收益 |
客户端缓存是「最后一公里」优化。先确保 Redis 本身、连接池、序列化已优化到位,再引入本地缓存——它带来的延迟收益最高,但管理成本也最高。
结语
- 客户端缓存把热数据放在应用本地,将单次访问延迟从毫秒级降到微秒级,代价是引入一致性复杂度。
- RESP3 协议通过推送消息(
>前缀)承载失效通知,开启 tracking 必须先协商HELLO 3。 - CLIENT TRACKING 记录客户端读过的 key,key 变更时推送失效事件,实现准实时失效。
- 四种模式各有用武之地:default 通用、opt-in/opt-out 精准、bcast 适合前缀型数据、redirect 适合连接池与集群。
- TTL 是推送失效的兜底,两者必须配合使用,覆盖断线窗口的一致性缺口。
- 主流客户端(Lettuce、go-redis、redis-py、ioredis)已支持 RESP3 与 tracking,实现前确认库版本与回调能力。
- 一致性权衡的决策依据是「脏读容忍度」:强一致场景禁用本地缓存,可容忍秒级弱一致才用 tracking。
- 落地时把握「容量上限 + TTL + 失效回调 + 重连全量失效」四条底线,并用命中率与 P99 压测验证收益。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。