《Redis 客户端缓存与 RESP3:降低往返延迟》

深入 Redis 服务端缓存与客户端缓存的差异、RESP2/RESP3 协议演进、CLIENT TRACKING 失效推送、default/opt-in/bcast/redirect 四种缓存模式、TTL 与失效处理、多语言客户端支持及一致性权衡。

一、服务端缓存 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)消息,成为客户端缓存失效通知的载体。

特性RESP2RESP3
批量字符串$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 提供四种模式,按「追踪范围」和「推送方向」区分:

模式语法追踪范围适用
defaultCLIENT TRACKING ON所有读过的 key 自动追踪通用场景
opt-inON OPTIN + CLIENT CACHING YES仅显式标记的 key精准控制
opt-outON OPTOUT + CLIENT CACHING NO除标记外全部少量排除
bcastON 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 生态支持矩阵

语言/客户端版本RESP3CLIENT TRACKING
Java Lettuce6.1+完整完整(失效订阅)
Java Jedis4.x部分需手动处理
Go go-redis/v9v9.1+完整完整
Python redis-py4.2+完整完整
Node ioredis5.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 常见误区

误区正确做法
所有数据都本地缓存只缓存热点小数据
不设 TTLTTL 是断线窗口唯一保障
忽略连接池每个连接单独开 tracking
集群直接用 default优先 bcast + prefix 或 redirect
不做压测用命中率与 P99 验证收益

客户端缓存是「最后一公里」优化。先确保 Redis 本身、连接池、序列化已优化到位,再引入本地缓存——它带来的延迟收益最高,但管理成本也最高。

结语

  1. 客户端缓存把热数据放在应用本地,将单次访问延迟从毫秒级降到微秒级,代价是引入一致性复杂度。
  2. RESP3 协议通过推送消息(> 前缀)承载失效通知,开启 tracking 必须先协商 HELLO 3。
  3. CLIENT TRACKING 记录客户端读过的 key,key 变更时推送失效事件,实现准实时失效。
  4. 四种模式各有用武之地:default 通用、opt-in/opt-out 精准、bcast 适合前缀型数据、redirect 适合连接池与集群。
  5. TTL 是推送失效的兜底,两者必须配合使用,覆盖断线窗口的一致性缺口。
  6. 主流客户端(Lettuce、go-redis、redis-py、ioredis)已支持 RESP3 与 tracking,实现前确认库版本与回调能力。
  7. 一致性权衡的决策依据是「脏读容忍度」:强一致场景禁用本地缓存,可容忍秒级弱一致才用 tracking。
  8. 落地时把握「容量上限 + TTL + 失效回调 + 重连全量失效」四条底线,并用命中率与 P99 压测验证收益。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 《Redis 容灾与备份恢复:RDB/AOF 备份、复制与演练》
  2. 《Redis 对象编码与内存优化:listpack 与编码转型》
  3. 《Redis 管道、事务与批量优化:从 N 次 RTT 到一次》