qinghuan
← 缓存的操作与结果目录

SPRING CACHE / 补充讲义 3 / 7

本地缓存返回对象的可变性与隔离验收

通过引用、按值保存和嵌套对象对照,检查展示加工是否污染缓存。

从缓存拿到一个列表之后,展示层若原地排序或脱敏,下一次请求会不会看到改动?这个问题与缓存键是否正确不同:键相同且命中成功,仍可能把同一个可变对象交给多个调用方。

资料基于 Spring Framework 7.0.10。原始核查:2026 年 10 月 24 日;发布整理复核:2026 年 10 月 30 日 JST。以下为本地 Java 验收设计,尚未运行,也未测量复制成本。

容器线程安全不代表值对象隔离

ConcurrentMapCache 默认保存对象引用。按值保存时,条目需要可序列化;ConcurrentMapCacheManager 的 storeByValue 默认是 false,改变它会重建已有缓存。缓存对象契约与管理器配置给出了这些前提。

因此,没有再次 put 也可能改变后续命中的内容:调用方只是修改了缓存与调用方共同持有的对象。线程安全的 Map 并不自动保护对象内部的列表、字段或其他可变成员。

先确认按值模式真的启用了

若使用 Spring 管理的 ConcurrentMapCacheManager Bean,容器会处理 BeanClassLoaderAware 生命周期。手工 new 的教学夹具需要补齐这一步:固定版本实现通过 setBeanClassLoader 建立序列化委托,创建缓存时再根据 storeByValue 选择它。

下面是未运行的夹具片段,用于明确初始化顺序;省略了导入和测试断言,不能当作独立程序直接运行:

文中示例 · java
var manager = new ConcurrentMapCacheManager();
manager.setBeanClassLoader(ConcurrentMapCacheManager.class.getClassLoader());
manager.setStoreByValue(true);
var cache = (ConcurrentMapCache) manager.getCache("shops");
// 测试先断言 cache != null 且 cache.isStoreByValue() 为 true。

只观察管理器上的布尔配置不足以证明实际缓存按值存储;应检查拿到的 ConcurrentMapCache。这一补充来自固定管理器实现与缓存实现。

练习一:分别修改原值和读出的值

创建两个独立管理器,一个保留默认按引用模式,另一个正确初始化为按值模式。以固定商户 ID 为键,以 ArrayList<String> 为可序列化假值,先放入一个公开标签。

  1. put 后修改最初的列表,再调用 get。
  2. 修改第一次 get 返回的列表,再次调用 get。
  3. 分别记录每次对象身份和元素内容,比较两组。

预期按引用组暴露共享改动,按值组保持存入快照;必须在项目锁定版本中运行后再填写实测列。所有数据都用假商户,不需要接入数据库或模型服务。

练习二:浅复制是否足够

给展示层实现不修改缓存原对象的转换。先用全是 String 的列表验证复制隔离,再换成含可变嵌套字段的假 DTO,检查外层复制后是否仍共享内部对象。连续查询同一商户,第二次结果应不受第一次展示加工影响。

另用不可序列化假值观察按值模式的实际异常。若测试运行中切换 storeByValue,应从管理器重新取得缓存,并检查旧条目是否还在;不能把配置切换当作无损迁移。

应用边界

不可变契约应覆盖嵌套成员,不能仅凭 record 或只读列表外壳宣布已隔离。Redis、Caffeine 等提供者的复制与序列化行为需要分别核查;本地对象身份结论不能直接迁移过去,也不构成跨进程一致性保证。

本节用于识别别名和隔离问题,不建议仅为练习在生产开启 Java 序列化或升级版本。来源独立发布日期未标明。前置内容见缓存键与加载范围。