4. 缓存领域模型(二):缓存聚合(Cache Aggregate)与仓储(Repository)模式设计

在上一节中,我们完成了缓存领域的基础实体与值对象建模。本节将引入两个更高级的战术模式——缓存聚合(Cache Aggregate)与缓存仓储(Cache Repository),它们共同构成了缓存一致性治理的核心领域骨架。聚合负责封装缓存数据的生命周期与不变规则,仓储则提供对聚合的持久化与检索抽象,使上层应用与底层缓存基础设施彻底解耦。

4.1 缓存聚合:从散落缓存到有界一致性边界

在传统缓存实践中,开发者往往直接操作 Redis 的 SET/GET 命令,缓存项之间缺乏关联约束。当业务要求“用户信息与用户权限必须同时更新或同时失效”时,散落操作极易导致部分更新、脏读等一致性问题。

缓存聚合将一组具有强一致性关联的缓存实体和值对象封装为一个整体,对外暴露唯一的聚合根(Aggregate Root)。外部只能通过聚合根访问内部缓存数据,所有变更必须经过聚合根校验,从而保证缓存边界内的数据始终满足业务不变规则。

核心原则:缓存聚合的边界就是缓存一致性的保证边界。聚合内数据要么全部一致,要么全部失效,不存在中间状态。

4.1.1 缓存聚合的构成要素

要素 说明 缓存场景示例
聚合根(Aggregate Root) 聚合的唯一入口,持有全局标识符 UserCacheAggregate 以 userId 为根标识
实体(Entity) 具有唯一标识的可变对象,生命周期由聚合根管理 UserProfile、UserPermission
值对象(Value Object) 不可变、无标识、通过属性值相等性判断 Address、RoleSet
不变规则(Invariant) 聚合内必须始终满足的业务约束 用户角色变更后,权限缓存必须同步刷新

4.1.2 缓存聚合的版本控制与并发安全

缓存聚合必须内置乐观并发控制机制。每次读取聚合时获取版本号(如 Redis 的 CAS token 或自定义递增序列),写入时校验版本号是否匹配。若版本冲突,说明其他线程已修改聚合,当前操作应重试或回滚。

// 缓存聚合根接口定义
public interface CacheAggregate<TId> {
    TId getAggregateId();
    long getVersion();
    boolean tryLock(long leaseTimeMs);
    void unlock();
    boolean isDirty();
    void markClean();
}
实践建议:在 Redis 中,可以使用 WATCH/MULTI/EXEC 事务或 Lua 脚本实现聚合级别的原子操作。对于跨缓存实例的聚合,建议引入分布式锁(如 Redlock)保护聚合根。

4.2 缓存仓储模式:领域层与缓存基础设施的桥梁

仓储(Repository)模式在 DDD 中用于将领域对象的存储与检索从领域层中剥离。在缓存领域,缓存仓储封装了对缓存聚合的 CRUD 操作,使领域服务无需关心底层是 Redis、Memcached 还是本地缓存。

4.2.1 缓存仓储的核心职责

  • 聚合持久化:将缓存聚合整体序列化后写入缓存存储,确保聚合内所有实体和值对象一次性保存。
  • 聚合重建:从缓存中读取原始数据,反序列化并重建完整的聚合对象,同时恢复版本号。
  • 一致性保障:在写入时执行版本校验,在读取时处理缓存穿透、击穿、雪崩等异常场景。
  • 失效策略管理:为聚合设置 TTL(生存时间),并在数据源变更时主动失效或刷新聚合。
// 缓存仓储接口示例
public interface CacheRepository<TAggregate, TId> where TAggregate : CacheAggregate<TId> {
    TAggregate findById(TId id);
    void save(TAggregate aggregate);
    void delete(TId id);
    boolean exists(TId id);
    // 批量操作
    Map<TId, TAggregate> findByIds(Collection<TId> ids);
    void saveAll(Collection<TAggregate> aggregates);
}

4.2.2 缓存仓储的实现层次

一个完整的缓存仓储实现通常分为三层:

  1. 序列化层:将聚合对象转换为字节流或 JSON 字符串。推荐使用 Protocol Buffers 或 MessagePack 以提升性能。
  2. 存储适配层:封装具体缓存客户端的 API 调用,如 Redis 的 Jedis、Lettuce 或 Spring Data Redis。
  3. 聚合管理层:处理版本控制、锁获取、脏标记检查等聚合生命周期逻辑。
常见陷阱:不要在仓储实现中混入业务逻辑。仓储只负责“存”和“取”,不负责“如何更新缓存”或“何时失效缓存”。业务规则应放在领域服务或应用服务中。

4.3 缓存聚合与仓储的协作流程

下图展示了缓存聚合与仓储在典型读写场景中的协作关系:

缓存聚合与仓储协作流程 应用服务 领域服务 缓存仓储 缓存聚合 Redis / 缓存存储 调用 findById / save 重建 / 持久化 序列化读写 流程说明: 1. 应用服务调用领域服务,传入聚合 ID。 2. 领域服务通过缓存仓储的 findById() 获取聚合。 3. 缓存仓储从 Redis 读取原始数据,反序列化并重建缓存聚合(含版本号)。 4. 领域服务修改聚合内的实体/值对象,聚合根校验不变规则。 5. 领域服务调用仓储 save(),仓储执行版本校验后序列化写入 Redis。 6. 若版本冲突,仓储抛出并发异常,领域服务决定重试或回滚。

4.4 缓存聚合的设计原则与反模式

原则 正确做法 反模式
聚合边界最小化 只包含必须同时一致的缓存项 将整个用户模块的所有缓存塞入一个聚合
聚合根唯一性 每个聚合只有一个根实体,外部通过根标识访问 允许外部直接修改聚合内的子实体
最终一致性容忍 跨聚合的缓存允许短暂不一致 用分布式事务强制跨聚合强一致
仓储接口纯抽象 仓储接口只定义领域语义的操作 仓储接口暴露 setExpire()、increment() 等缓存技术细节

4.5 缓存仓储的序列化与反序列化策略

缓存仓储的性能瓶颈往往出现在序列化环节。以下是三种主流序列化方案的对比:

方案 序列化速度 数据体积 跨语言支持 推荐场景
JSON (Jackson/Gson) 中等 较大 优秀 调试友好、异构系统集成
Protocol Buffers 快 小 优秀 高性能、跨语言微服务
Kryo 极快 极小 仅 Java Java 单体应用、极致性能
版本兼容性:无论选择哪种序列化方案,都必须在序列化数据中嵌入 schema 版本号。当聚合结构演进时,仓储可以识别旧版本数据并执行升级反序列化,避免缓存数据与代码不兼容导致的崩溃。

4.6 缓存聚合的失效级联策略

当数据源发生变更时,缓存聚合需要整体失效。但聚合内的不同实体可能具有不同的失效敏感度。下图展示了一种基于聚合根的级联失效策略:

缓存聚合级联失效策略 聚合根 (UserCache) UserProfile 实体 UserPermission 实体 Address 值对象 TTL: 300s TTL: 120s (敏感) TTL: 600s 失效规则:聚合根失效时,所有子实体和值对象一并失效。子实体可独立设置更短的 TTL 实现“软过期”,但聚合根 TTL 是硬上限。

4.7 本章小结

  • 缓存聚合将强关联的缓存项封装为一致性边界,通过聚合根统一管理生命周期和不变规则。
  • 缓存仓储提供领域语义的持久化抽象,使领域层与缓存基础设施解耦,同时内置版本控制和并发安全机制。
  • 聚合边界应遵循最小化原则,仓储接口应保持纯抽象,避免泄露技术细节。
  • 序列化方案的选择需平衡性能、体积和跨语言需求,并始终携带 schema 版本号以支持演进。
  • 级联失效策略确保聚合内数据要么整体一致,要么整体失效,杜绝部分更新导致的脏数据。