
MCP工具治理 多实例 / Redis pub-sub / 缓存一致性
本篇是《MCP实战手记》篇12(企业级Toolbox治理)的延伸篇,只拆「多实例一致性」一个点:把篇12 §8 的 Redis 一致性方案往下挖一层------本地 Caffeine 缓存 + pub/sub 失效,以及 Redis 挂了怎么降级。
0. 为什么不能只直读 Redis
篇12 §8 实测证明:两实例 A 播种 / B 只读,GET /api/toolboxes 逐字节一致,灰度与弃用状态跨实例同步------因为查询直走 Redis。
但"每次查询都打 Redis"在生产有代价:①Redis 抖动拖慢发现接口;②注册/QPS 高时 Redis 是单点瓶颈。正确做法是查询走本地缓存,写操作才落 Redis 并广播失效。
本文只拆这一层:本地缓存选 Caffeine、失效靠 pub/sub、Redis 不可用走降级白名单。

1. 直读 Redis 的局限
架构A(直读 Redis,篇12 §8 实测):
A.query -> Redis
B.query -> Redis
一致性: 强一致 延迟: 每次 ~1 RTT 风险: Redis 抖全家抖
每次 list/resolve 都访问 Redis,实例数↑时 Redis 连接与 QPS 线性上涨。
2. 本地缓存 + pub/sub 失效
写操作(注册/反注册/灰度/弃用)落 Redis 并 publish("toolbox-events", ...);每个实例监听事件,清掉本地 Caffeine 对应 key。
java
// 本地缓存:Caffeine,写操作后由事件失效。缓存键 = id@v,避免不同 caller 的 hintVersion 命中错误版本
Cache<String, ToolboxDefinition> local = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)).build();
// 监听 Redis pub/sub,清本地缓存(按 id 失效该工具全部版本)
redis.subscribe("toolbox-events", evt -> {
ToolboxId id = parseId(evt); // 解析 "gray:..." / "deprecate:..." / "lifecycle:..."
local.invalidateAll(k -> k.startsWith(id + "@")); // 该 id 所有版本失效
});
// 读:先本地,miss 才回源 Redis(键含版本,杜绝错版本)
ToolboxDefinition resolve(ToolboxId id, String v) {
return local.get(id + "@" + v, k -> registry.resolveFromRedis(id, v));
}
关键状态(deprecate/remove)订阅到事件后即
invalidateAll该 id,下次 resolve 必回源;即便 pub/sub 丢事件,5min TTL 兜底------但已下线工具最多暴露 5min,关键业务可缩短 TTL 或对 REMOVED 强制回源(bypass 缓存)。
效果:①热工具命中本地缓存,发现接口延迟从 ~1 RTT 降到亚毫秒;②写操作仍强一致(Redis 为准 + 事件失效);③Redis QPS 降一个量级。
3. Redis 不可用降级白名单
Redis 挂了,本地缓存可能已失效、回源会抛错。降级策略:保留一份"最后已知好状态"白名单,Redis 不可用时只读不写。
java
// Redis 健康状态缓存(如 1s 探活一次),避免每请求多一次探测开销
boolean redisUp = health.isUpCached(); // 内部 1s 刷新,非每请求探测
ToolboxDefinition resolve(ToolboxId id, String v) {
if (redisUp) return local.get(id + "@" + v, k -> registry.resolveFromRedis(id, v));
// 降级:返回本地缓存残值(不回源),新注册被拒
ToolboxDefinition cached = local.getIfPresent(id + "@" + v);
if (cached == null) throw new ServiceUnavailableException("Redis 降级中,缓存未命中");
return cached;
}
降级窗口内:发现/调用可读(用缓存残值),但注册/灰度/弃用等写操作暂停------保证"读可用、写收敛",与篇10 高可用思路一致。Redis 探活恢复后自动切回读写模式,无需人工干预(health.isUpCached 翻转即恢复)。
4. 跨实例一致性验证(复用篇12 §8 两实例实测)
沿用篇12 §8 的 Docker 双实例实测,加一层缓存验证:
A 注册 get_report@2.0.0 -> Redis 写 + publish 事件
B 本地缓存命中 get_report -> 事件到达 B,local.invalidate
B 再 resolve(get_report) -> 回源 Redis 命中 2.0.0,与 A 一致
kill Redis -> A/B 切降级,读缓存残值不 500
Redis 恢复 -> 写操作恢复,下次 resolve 直接回源 Redis 拿最新(自愈,无需补发事件)
验证点:①事件失效使 B 及时拿到新状态;②降级时不 500;③恢复后无需重启即回到强一致。
5. 边界声明
- 本机已验证:本地 Caffeine 缓存 + pub/sub 失效逻辑、降级开关读路径(Redis mock 注入)。
- 待生产验证:真实 Redis 抖动下的缓存命中率、降级窗口时长对调用方的影响、事件丢失补偿(本期靠 TTL 兜底)。
- Redis 持久化:治理状态(注册/灰度/弃用)以 Redis 为存储时需开 AOF/RDB 或以 DB 为源,否则 Redis 重启丢状态、多实例回到全量稳定版。
- 本文只拆多实例一致性一层;两实例逐字节一致详见篇12 §8,高可用设计见篇10。
同专题推荐: