1.1 缓存契约概念
1.1.1 什么是缓存契约
缓存契约(Cache Contract)是分布式系统中缓存客户端与缓存服务端之间的一组显式约定,它定义了数据的存储格式、过期策略、序列化方式、版本标识以及一致性保证。契约不是代码实现,而是行为与结构的规范,确保不同团队、不同服务对同一份缓存数据的读写行为达成一致。
核心定义:缓存契约 = 数据结构约定 + 生命周期规则 + 版本标识 + 访问协议
在领域驱动设计(DDD)视角下,缓存契约对应限界上下文(Bounded Context)之间的数据交换协议。每个限界上下文拥有自己的缓存空间,契约明确了哪些数据可以跨上下文共享、以何种格式共享、何时失效。
| 契约维度 | 说明 | Redis 中的体现 |
|---|---|---|
| 数据结构 | 缓存值的类型、字段、嵌套关系 | String / Hash / JSON 序列化 |
| 序列化 | 对象与字节流的转换规则 | JSON / Protobuf / MessagePack |
| 过期策略 | TTL、最大空闲时间、主动淘汰 | EXPIRE / TTL / LRU |
| 版本标识 | 数据版本号或时间戳 | 自定义 version 字段 |
| 一致性级别 | 最终一致性 / 读写一致性 / 强一致性 | Redis 主从 + 哨兵 |
1.1.2 契约在分布式系统中的角色
分布式系统中,缓存契约承担以下关键角色:
- 解耦生产者与消费者:数据写入方(生产者)与读取方(消费者)通过契约约定格式,无需感知对方内部实现。
- 版本兼容保障:契约中的版本字段允许系统同时存在多个数据版本,支持灰度发布与回滚。
- 故障隔离:当缓存数据格式异常时,契约校验可以快速拒绝无效数据,防止脏数据扩散。
- 性能优化边界:契约定义了哪些数据适合缓存、缓存多久,避免缓存滥用导致内存膨胀。
- 领域边界映射:每个限界上下文维护自己的缓存契约,防止跨上下文的数据耦合。
实践建议:在微服务架构中,每个服务应独立声明其缓存契约,并通过共享库(Shared Kernel)或契约测试(Contract Testing)保证一致性。避免在多个服务间直接共享 Redis Key 前缀或序列化配置。
1.1.3 契约与缓存的关联
缓存与契约的关系可以概括为“无契约,不缓存”。没有显式契约的缓存系统会逐渐退化为不可维护的“数据沼泽”:
- 格式漂移:不同版本的代码写入不同格式的数据,读取时无法反序列化。
- 过期混乱:部分数据 TTL 过长,部分过短,导致缓存命中率波动。
- 版本冲突:旧版本服务写入旧格式数据,新版本服务读取后产生异常。
- 领域泄漏:一个上下文的内部数据被另一个上下文误读,破坏封装性。
缓存契约通过以下机制建立关联:
| 关联维度 | 缓存侧 | 契约侧 |
|---|---|---|
| 写入 | 序列化 + 设置 TTL | 校验版本号、字段完整性 |
| 读取 | 反序列化 + 命中判断 | 校验版本兼容性、字段类型 |
| 失效 | 主动删除 / TTL 过期 | 触发版本升级或数据迁移 |
| 监控 | 缓存命中率、内存使用 | 契约违反次数、版本分布 |
1.1.4 缓存契约的版本化
版本是缓存契约的核心要素。每次数据结构变更(新增字段、修改类型、删除字段)都应伴随版本号递增。版本化策略通常有两种:
| 策略 | 实现方式 | 适用场景 | 风险 |
|---|---|---|---|
| 内联版本 | 在缓存值中嵌入 version 字段 | 单服务、小团队 | 版本膨胀后解析复杂 |
| 外置版本 | 使用独立的 Redis Key 存储版本映射 | 多服务、跨团队 | 额外查询开销 |
| 混合版本 | 内联主版本 + 外置兼容性矩阵 | 大规模分布式系统 | 运维复杂度高 |
常见陷阱:不要将版本号与业务数据混在一起作为 Redis Key 的一部分(如
user:123:v2)。这会导致旧版本数据无法被自动清理,且 Key 数量膨胀。正确的做法是将版本号放在 Value 内部或使用独立的版本映射 Key。
1.1.5 契约驱动的缓存设计原则
- 显式声明:每个缓存 Key 的契约必须在代码中显式声明,禁止隐式约定。
- 向前兼容:新版本契约必须能读取旧版本数据,旧版本代码遇到新版本数据应优雅降级。
- 最小化共享:跨限界上下文的缓存数据应尽量少,优先使用事件驱动同步而非共享缓存。
- 契约即文档:缓存契约应自动生成文档,供上下游团队查阅。
- 可测试性:每个缓存契约都应配套单元测试和契约测试,验证序列化/反序列化正确性。
1.1.6 代码示例:缓存契约定义
以下是一个基于 Redis Hash 的缓存契约示例,包含版本标识和序列化校验:
// 缓存契约定义(Java 示例)
public class UserCacheContract {
// 当前契约版本
public static final int CURRENT_VERSION = 2;
// 缓存数据结构
public static class UserCacheValue {
private int version;
private String userId;
private String userName;
private String email;
private long lastUpdated;
// 序列化方法
public Map<String, String> toHash() {
Map<String, String> hash = new HashMap<>();
hash.put("version", String.valueOf(CURRENT_VERSION));
hash.put("userId", userId);
hash.put("userName", userName);
hash.put("email", email);
hash.put("lastUpdated", String.valueOf(lastUpdated));
return hash;
}
// 反序列化 + 版本校验
public static UserCacheValue fromHash(Map<String, String> hash) {
int version = Integer.parseInt(hash.get("version"));
if (version > CURRENT_VERSION) {
// 遇到未来版本,降级处理
throw new CacheContractException("未知契约版本: " + version);
}
// 版本兼容转换逻辑
UserCacheValue value = new UserCacheValue();
value.version = version;
value.userId = hash.get("userId");
value.userName = hash.get("userName");
value.email = hash.getOrDefault("email", ""); // 兼容旧版本无 email 字段
value.lastUpdated = Long.parseLong(hash.get("lastUpdated"));
return value;
}
}
}
关键点:上述代码中
fromHash 方法使用 getOrDefault 处理旧版本缺失字段,实现了向前兼容。版本号 CURRENT_VERSION 作为常量集中管理,避免散落在业务代码中。