第2章:缓存抽象层的系统定位:分层、边界与交互

缓存技术选型从单一 Redis 演进到多级缓存(本地 + 分布式)、多供应商混合部署时,业务代码会散落大量缓存访问逻辑,导致基础设施耦合、故障扩散、扩展困难。本章聚焦缓存抽象层的系统定位:它在业务层与具体缓存基础设施之间如何插入,职责边界如何划分,以及与数据访问层的交互关系。

2.1 为什么需要插入一个统一的接口层

没有抽象层时,业务代码直接依赖 Jedis、Lettuce、Redisson 或 Memcached Client。这种模式带来三个核心痛点:

痛点 具体表现 直接后果
基础设施耦合 业务方法内直接创建 Redis 连接、调用 set/get 方法 更换缓存中间件时需要修改几乎所有业务模块
故障扩散 缓存连接中断或超时时,业务线程被阻塞或抛出异常 缓存不可用导致业务整体不可用
缺少治理点 无法统一做序列化、压缩、监控、限流、降级 指标分散,运维困难,容量规划无依据

缓存抽象层以接口为中立的契约,业务层只依赖该接口,具体实现在运行时注入。该模式来自经典的 依赖倒置原则:高层模块(业务)不应依赖低层模块(缓存驱动),双方都应依赖抽象。

核心价值: 抽象层将缓存中间件变成可替换的“后端实现”,业务代码无需感知底层是 Redis、Memcached 还是本地 ConcurrentHashMap。

2.2 职责边界划分

缓存抽象层不是万能中间层,必须清晰界定它做什么和它不做什么。下表列出三层职责边界:

层级 职责范围 不负责的事项
业务层 定义缓存键(Key)、理解缓存数据含义、决定缓存策略(缓存什么、何时失效) 不连接任何缓存的细节,不处理序列化
缓存抽象层 提供泛化的 get/set/delete/expire 接口;管理序列化与反序列化;实现降级、熔断、监控统计;控制批量操作;统一异常处理 不解释业务语义,不触发缓存反转,不参与事务边界
缓存基础设施 实际数据存储与过期驱逐;网络连接池管理;哨兵/集群拓扑维护;持久化 不理解数据结构含义,不负责业务降级逻辑
架构原则: 抽象层对业务暴露的接口必须是“缓存原语”(get/set/delete),而不是“业务行为”(getUserById)。业务行为编排留在业务层。

2.3 与数据访问层(DAO)的交互关系

数据访问层(DAO/Repository)通常负责关系数据库或 NoSQL 的主数据读写。缓存抽象层与 DAO 的典型交互遵循 旁路缓存模式(Cache-Aside):

  1. 读路径:业务层调用抽象层 get → 未命中 → 业务层调用 DAO 加载数据 → 业务层调用抽象层 set 写入缓存 → 返回。
  2. 写路径:业务层先调用 DAO 更新持久化存储 → 再调用抽象层 delete 删除缓存(或更新缓存)。

控制流始终由业务层编排,抽象层不主动触发 DAO 调用。这保持了缓存层与数据存储层的信任链路隔离:缓存层不应被当作“可回写的数据存储”,它只做临时加速。

风险提醒: 部分设计让抽象层内嵌“缓存未命中时自动调用 DAO”的逻辑。这会引入循环依赖和延迟膨胀,使抽象层职责不清。除非在特殊场景(如完全读透),否则应当避免。

2.4 系统分层架构与数据流

下面的 SVG 图展示缓存抽象层在整体架构中的夹层位置,以及读写两路的典型数据流与控制流:

缓存抽象层系统定位:分层与数据流 业务层 Service / Controller / BFF 编排读写路径,定义 Key 与策略 缓存抽象层 CachePort / CacheManager 序列化 · 降级 · 监控 · 统一异常 缓存基础设施 Redis · Memcached · Local 存储 · 过期 · 集群 数据访问层 DAO / Repository DB · NoSQL · 持久化存储 接口调用(get/set/delete) 原语操作 不直接调用(依赖业务编排) 读路径(Cache-Aside) ① 业务层 → 抽象层.get(key) ② 未命中 → 业务层 → DAO.load(key) ③ 业务层 → 抽象层.set(key, data) ④ 抽象层序列化写入缓存基础设施 写路径(Cache-Aside) ① 业务层 → DAO.update(primary store) ② 业务层 → 抽象层.delete(key) ③ 抽象层通知缓存基础设施移除数据 ④ 后续读路径触发重新加载

2.5 控制流与信任链路

控制流从业务层出发,依次穿透抽象层,最终到达缓存基础设施或数据访问层。关键信任链路约束如下:

  • 信任方向:业务层信任抽象层接口稳定;抽象层信任基础设施的连接能力;业务层同时信任 DAO 的数据完整性。
  • 降级解耦:抽象层内部捕获基础设施异常后,可将降级决策直接返回给业务层(如返回 null 或兜底缓存),业务层自行决定是否回源 DAO。抽象层不应在降级时主动调用 DAO。
  • 超时隔离:抽象层为每个缓存操作为单独的超时控制(如 10ms),防止缓存阻塞影响业务主链路。
信任链路原则: 抽象层对缓存基础设施故障保持“有损服务”态度——宁可降级穿透,也不让业务阻塞等待。

2.6 选型权衡:接口粒度与泛化

抽象层的接口设计有两派策略,适用于不同场景:

策略 接口粒度 典型接口 适配场景 风险点
薄接口 仅暴露原语 get, set, delete, expire 通用缓存加速,多供应商切换 无法表达缓存特有功能(如 Redis 的 LREM、SCAN)
厚接口 包含少量集合操作 multiGet, multiSet, atomicIncr 需要批量操作或原子递增的场景 抽象层膨胀,部分基础设施无法实现
泛化接口 完全与基础设施耦合 execute(RedisCommand) 需要利用特定缓存所有特性的场景 失去抽象意义,退化为“透传壳”
推荐组合: 以薄接口为主体,在抽象层扩展点中预留 CommandCallback 允许业务绕过抽象层执行特定指令,但需要通过独立 SPI 声明。

2.7 落地步骤与验收标准

落地步骤

  1. 定义接口契约:先设计 CachePort 接口(get/set/delete/expire),使用泛型 <V> 避免序列化泄漏。
  2. 实现桩模块:为一个缓存基础设施(如本地 HashMap)提供实现,用于单元测试与原型验证。
  3. 集成真实实现:实现 Redis 或 Memcached 适配器,将序列化逻辑放入抽象层。
  4. 嵌入测点与降级:在抽象层实现类中注入监控(命中率、延迟、错误计数)和降级规则。
  5. 业务迁移:将业务层原有的直接缓存调用替换为依赖 CachePort 接口。

验收标准

  • 业务层代码零直接引用 Redis/Memcached 客户端库。
  • 替换缓存基础设施时(如从单机 Redis 切换到集群 Redis),只修改依赖注入配置和实现 jar,不修改业务代码。
  • 缓存基础设施宕机时,业务接口返回降级数据(如空列表或本地缓存),不抛出连接异常。
  • 抽象层接口的延迟 P99 低于纯客户端调用延迟的 5%(微乎其微的损耗)。

2.8 风险点

  • 过度抽象:接口太泛,丢失缓存特有功能导致性能倒退。
  • 降级风暴:降级时大量回源 DAO,可能压垮数据库,需要配合限流。
  • 序列化陷阱:抽象层统一序列化可能忽略特定缓存的压缩协议,如 Redis 的 protobuf vs JSON。
  • 配置不一致:多缓存实例时,抽象层路由配置与基础设施拓扑不同步造成数据错乱。

缓存抽象层的正确系统定位是“夹层的隔离层与治理层”,不是全能网关,也不是透明代理。本章边界划分为后续实现提供了清晰的模块责任。