3. 缓存领域模型(一):缓存实体(Cache Entity)与值对象(Value Object)的建模
在 OpenAPI 场景下,缓存一致性治理的第一步是建立精确的领域模型。许多团队陷入缓存混乱的根本原因,是将“缓存”视为一个技术工具,而非领域概念。本节从领域驱动设计(DDD)的视角,将缓存抽象为缓存实体(Cache Entity)与值对象(Value Object),并给出明确的建模边界与治理规则。
3.1 为什么缓存需要领域建模?
传统缓存设计往往直接面向 Redis Key-Value 或本地 Map,导致以下问题:
- 身份模糊:同一个业务对象在缓存中可能被多个 Key 表示,无法追踪其生命周期。
- 变更失控:缓存更新时只更新了值,未更新标识,导致脏数据。
- 一致性缺失:没有区分“可替换的值”与“需追踪的实体”,缓存失效策略混乱。
核心原则:缓存中的每一个数据项,都应明确其领域角色——它是需要被追踪身份的“实体”,还是仅作为快照的“值对象”。
3.2 缓存实体(Cache Entity)
定义:缓存实体是具有唯一标识(Cache ID)且生命周期需要被追踪的缓存对象。即使其属性值发生变化,该对象在缓存中的身份保持不变。
3.2.1 建模特征
| 特征 | 说明 | OpenAPI 场景示例 |
|---|---|---|
| 唯一标识 | 每个实体拥有全局唯一的 Cache ID(如 user:uuid:123) |
order:orderId:ORD20250301 |
| 可变属性 | 属性值可以更新,但标识不变 | 订单状态从“待支付”变为“已支付” |
| 生命周期管理 | 需要显式创建、更新、删除操作 | 用户注销后,缓存实体需主动失效 |
| 一致性责任 | 实体变更时,必须同步更新或失效缓存 | 数据库订单状态变更 → 触发缓存实体更新 |
3.2.2 代码建模示例
// 缓存实体:具有唯一标识的缓存对象
public class CacheEntity<T> {
private final String cacheId; // 全局唯一标识
private final String entityType; // 实体类型(如 "user", "order")
private T data; // 业务数据
private long version; // 乐观锁版本号
private Instant lastModified; // 最后修改时间
public CacheEntity(String cacheId, String entityType, T data) {
this.cacheId = cacheId;
this.entityType = entityType;
this.data = data;
this.version = 1L;
this.lastModified = Instant.now();
}
// 更新数据时版本号递增
public void updateData(T newData) {
this.data = newData;
this.version++;
this.lastModified = Instant.now();
}
// 通过 cacheId 判断身份是否相同
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheEntity<?> that = (CacheEntity<?>) o;
return Objects.equals(cacheId, that.cacheId);
}
@Override
public int hashCode() {
return Objects.hash(cacheId);
}
}
3.3 缓存值对象(Cache Value Object)
定义:缓存值对象是没有唯一标识、仅通过属性值来定义自身的缓存对象。两个值对象如果所有属性值相同,则视为同一个对象。
3.3.1 建模特征
| 特征 | 说明 | OpenAPI 场景示例 |
|---|---|---|
| 无标识 | 不持有 Cache ID,仅由属性值组成 | Address{city, district, detail} |
| 不可变性 | 创建后不应修改,需要变更时直接替换整个对象 | 用户地址变更 → 替换整个地址值对象 |
| 值相等性 | 通过所有属性比较判断是否相等 | 两个 Address 对象属性完全一致则视为相同 |
| 共享安全 | 可被多个实体引用,无需担心副作用 | 多个订单共享同一个收货地址值对象 |
3.3.2 代码建模示例
// 缓存值对象:无标识,不可变,通过值比较
public final class CacheValueObject<T> {
private final T data; // 业务数据
private final Instant cachedAt; // 缓存时间戳
public CacheValueObject(T data) {
this.data = data;
this.cachedAt = Instant.now();
}
public T getData() {
return data;
}
public Instant getCachedAt() {
return cachedAt;
}
// 值对象通过所有属性比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheValueObject<?> that = (CacheValueObject<?>) o;
return Objects.equals(data, that.data);
}
@Override
public int hashCode() {
return Objects.hash(data);
}
}
3.4 实体 vs 值对象:选择决策矩阵
| 判断维度 | 选择缓存实体 | 选择缓存值对象 |
|---|---|---|
| 是否需要追踪变更历史? | 是 | 否 |
| 数据是否会被独立更新? | 是 | 否(整体替换) |
| 多个消费者是否共享同一份数据? | 是(需同步) | 是(无副作用) |
| 缓存失效是否需要精确控制? | 是(按 ID 失效) | 否(按 Key 整体失效) |
| 典型 OpenAPI 场景 | 用户会话、订单状态、商品库存 | 配置信息、静态字典、计算快照 |
实践建议:在 OpenAPI 网关层,用户令牌(Token)应建模为缓存实体(需追踪登录状态),而接口限流配额应建模为缓存值对象(仅需当前值)。
3.5 缓存领域模型关系图
下图展示了缓存实体与值对象在缓存层中的协作关系:
3.6 实体与值对象的协作模式
在 OpenAPI 缓存治理中,实体与值对象通常以组合方式出现:
- 实体持有值对象:用户实体(Entity)持有地址值对象(Value Object)。地址变更时,直接替换整个值对象,用户实体的版本号递增。
- 值对象独立缓存:高频读取但极少变更的配置数据(如接口超时时间),直接作为值对象缓存,不绑定实体。
- 实体聚合值对象列表:订单实体包含商品列表(值对象集合),商品信息变更不影响订单实体的身份。
常见陷阱:不要将值对象“升级”为实体。例如,将地址值对象赋予唯一 ID 并追踪其变更,会导致缓存模型膨胀且一致性维护成本剧增。值对象的核心价值在于“无状态、可替换”。
3.7 建模后的治理收益
| 治理维度 | 建模前(混乱) | 建模后(清晰) |
|---|---|---|
| 缓存 Key 设计 | 随意拼接字符串,无规范 | 实体 Key 包含类型+ID,值对象 Key 包含上下文 |
| 失效策略 | 全局 TTL 或手动删除 | 实体按 ID 精确失效,值对象按 Key 整体失效 |
| 并发更新 | 直接覆盖,丢失更新 | 实体使用版本号乐观锁,值对象整体替换 |
| 一致性验证 | 难以追溯数据来源 | 实体可追踪变更历史,值对象可校验时间戳 |