当前位置: 首页 > news >正文

spring boot框架中本地缓存@Cacheable原理与踩坑点详细解析

本地缓存解析:原理、问题与最佳实践

问题描述:最近在项目中获取被@Cacheable注解的缓存数据,并对返回的缓存数据做修改相关的中间操作,最后发现输出的结果跟每次调用后结果都不一致,最终发现spring中的本地缓存受Java对象引用特性的影响,实际操作的是缓存数据指向的地址内容,修改的结果会直接修改缓存对象里的内容,由此记下整理的关于spring本地缓存的 易踩坑点

Spring Boot 本地缓存使用规范与安全指南

1. 本地缓存概述

1.1 什么是本地缓存

本地缓存是指将数据存储在应用进程内存中的缓存机制,与分布式缓存(如Redis)相对。在Spring Boot中,常用的本地缓存实现包括:

  • Caffeine - 高性能Java缓存库
  • Ehcache - 成熟的Java缓存解决方案
  • ConcurrentMap - 基于ConcurrentHashMap的简单缓存

1.2 本地缓存的优势与风险

优势:

  • 极快的访问速度(内存级)
  • 无网络开销
  • 部署简单,无需额外基础设施

风险:

  • 对象引用共享 - 缓存对象可能被意外修改
  • 内存限制 - 受JVM堆内存限制
  • 集群一致性 - 多实例环境下数据不一致

2. @Cacheable 工作机制详解

2.1 缓存读取流程

@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {// 此方法体仅在缓存未命中时执行return userRepository.findById(id).orElse(null);
}

执行流程:

  1. 根据cacheName和key生成缓存键
  2. 在缓存中查找对应数据
  3. 缓存命中:直接返回缓存对象,不执行方法体
  4. 缓存未命中:执行方法体,将结果存入缓存

2.2 关键特性说明

  • 不会自动更新:缓存命中时不会重新执行方法更新缓存
  • 不会反写数据:修改返回对象不会自动更新缓存
  • 引用共享风险:本地缓存返回的是对象引用,而非副本

3. 对象修改风险与验证

3.1 风险演示

@Service
public class UserService {@Cacheable(value = "users", key = "#id")public User getUser(Long id) {return new User(id, "原始名字", "email@example.com");}
}// 风险验证测试
@SpringBootTest
class CacheRiskTest {@Autowiredprivate UserService userService;@Testvoid demonstrateCacheModificationRisk() {// 第一次获取 - 存入缓存User user1 = userService.getUser(1L);// 第二次获取 - 从缓存读取(同一对象引用)User user2 = userService.getUser(1L);// 验证对象引用相同assertTrue(user1 == user2); // 通过!// 危险操作:修改对象属性user1.setName("被恶意修改的名字");// 第三次获取 - 缓存已被污染User user3 = userService.getUser(1L);assertEquals("被恶意修改的名字", user3.getName()); // 通过!}
}

3.2 风险影响范围

操作类型风险等级影响说明
修改对象属性🔴 高危直接污染缓存数据
修改集合内容🔴 高危影响缓存中的集合
基本类型操作🟢 安全基本类型不可变
String操作🟢 安全String对象不可变

4. 解决方案

4.1 防御性拷贝(推荐)

4.1.1 手动深拷贝
@Service
public class SafeUserService {@Cacheable(value = "users", key = "#id")public User getSafeUser(Long id) {User user = userRepository.findById(id).orElse(null);return user != null ? deepCopy(user) : null;}private User deepCopy(User original) {User copy = new User();copy.setId(original.getId());copy.setName(original.getName());copy.setEmail(original.getEmail());copy.setCreateTime(original.getCreateTime());// 嵌套对象也需要深拷贝if (original.getProfile() != null) {copy.setProfile(deepCopyProfile(original.getProfile()));}// 集合对象深拷贝if (original.getRoles() != null) {copy.setRoles(original.getRoles().stream().map(this::deepCopyRole).collect(Collectors.toList()));}return copy;}
}
4.1.2 序列化深拷贝
@Service
public class SerializationSafeService {private final ObjectMapper objectMapper = new ObjectMapper();@Cacheable(value = "users", key = "#id")public User getSerializationSafeUser(Long id) {User user = userRepository.findById(id).orElse(null);return deepCopyViaSerialization(user);}private <T> T deepCopyViaSerialization(T original) {if (original == null) return null;try {// 要求对象实现Serializable接口if (!(original instanceof Serializable)) {throw new IllegalArgumentException("对象必须实现Serializable接口");}byte[] bytes = objectMapper.writeValueAsBytes(original);return objectMapper.readValue(bytes, (Class<T>) original.getClass());} catch (Exception e) {throw new RuntimeException("深拷贝失败", e);}}
}

4.2 不可变对象模式

4.2.1 不可变DTO设计
/*** 不可变用户对象*/
public final class ImmutableUser {private final Long id;private final String name;private final String email;private final LocalDateTime createTime;private final List<ImmutableRole> roles;// 构造方法私有,通过工厂方法创建private ImmutableUser(Builder builder) {this.id = builder.id;this.name = builder.name;this.email = builder.email;this.createTime = builder.createTime;this.roles = Collections.unmodifiableList(builder.roles.stream().map(ImmutableRole::copyOf).collect(Collectors.toList()));}// 只有getter方法public Long getId() { return id; }public String getName() { return name; }public String getEmail() { return email; }public LocalDateTime getCreateTime() { return createTime; }public List<ImmutableRole> getRoles() { return roles; }// 工厂方法public static ImmutableUser copyOf(User user) {return new Builder().id(user.getId()).name(user.getName()).email(user.getEmail()).createTime(user.getCreateTime()).roles(user.getRoles()).build();}// Builder模式public static class Builder {private Long id;private String name;private String email;private LocalDateTime createTime;private List<Role> roles = new ArrayList<>();public Builder id(Long id) { this.id = id; return this; }public Builder name(String name) { this.name = name; return this; }public Builder email(String email) { this.email = email; return this; }public Builder createTime(LocalDateTime createTime) { this.createTime = createTime; return this; }public Builder roles(List<Role> roles) { this.roles = roles; return this; }public ImmutableUser build() {return new ImmutableUser(this);}}
}
4.2.2 使用不可变对象
@Service
public class ImmutableUserService {@Cacheable(value = "users", key = "#id")public ImmutableUser getImmutableUser(Long id) {User user = userRepository.findById(id).orElse(null);return user != null ? ImmutableUser.copyOf(user) : null;}
}

4.3 集合对象保护

4.3.1 列表保护
@Service
public class CollectionSafeService {@Cacheable(value = "allUsers")public List<User> getAllUsersSafe() {List<User> users = userRepository.findAll();// 返回不可修改的深拷贝列表return users.stream().map(this::deepCopy).collect(Collectors.collectingAndThen(Collectors.toList(),Collections::unmodifiableList));}@Cacheable(value = "userMap")public Map<Long, User> getUserMapSafe() {Map<Long, User> userMap = userRepository.findAllAsMap();// 返回不可修改的深拷贝Mapreturn userMap.entrySet().stream().collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey,entry -> deepCopy(entry.getValue())),Collections::unmodifiableMap));}
}

5. 高级保护方案

5.1 自定义保护性CacheManager

@Configuration
@EnableCaching
public class ProtectedCacheConfig {@Beanpublic CacheManager cacheManager() {CaffeineCacheManager defaultManager = new CaffeineCacheManager();defaultManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(Duration.ofHours(1)).maximumSize(1000));return new ProtectiveCacheManager(defaultManager);}
}/*** 保护性缓存管理器包装器*/
public class ProtectiveCacheManager implements CacheManager {private final CacheManager delegate;private final ObjectMapper objectMapper = new ObjectMapper();public ProtectiveCacheManager(CacheManager delegate) {this.delegate = delegate;}@Overridepublic Cache getCache(String name) {Cache originalCache = delegate.getCache(name);return new ProtectiveCacheWrapper(originalCache);}@Overridepublic Collection<String> getCacheNames() {return delegate.getCacheNames();}private class ProtectiveCacheWrapper implements Cache {private final Cache delegate;public ProtectiveCacheWrapper(Cache delegate) {this.delegate = delegate;}@Overridepublic ValueWrapper get(Object key) {ValueWrapper wrapper = delegate.get(key);if (wrapper == null) return null;Object value = wrapper.get();Object protectedValue = protectValue(value);return () -> protectedValue;}@Overridepublic <T> T get(Object key, Class<T> type) {T value = delegate.get(key, type);return type.cast(protectValue(value));}@Overridepublic void put(Object key, Object value) {Object protectedValue = protectValue(value);delegate.put(key, protectedValue);}// 其他方法实现...private Object protectValue(Object value) {if (value == null) return null;try {// 通过序列化实现深拷贝byte[] bytes = objectMapper.writeValueAsBytes(value);return objectMapper.readValue(bytes, value.getClass());} catch (Exception e) {// 拷贝失败,记录日志但继续使用原对象log.warn("缓存保护拷贝失败,使用原对象: {}", e.getMessage());return value;}}}
}

5.2 AOP拦截保护

@Aspect
@Component
@Slf4j
public class CacheProtectionAspect {private final ObjectMapper objectMapper = new ObjectMapper();/*** 保护@Cacheable方法的返回值*/@Around("@annotation(org.springframework.cache.annotation.Cacheable)")public Object protectCacheableResult(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();return deepCopyResult(result);}/*** 保护@CachePut方法的值参数和返回值*/@Around("@annotation(org.springframework.cache.annotation.CachePut)")public Object protectCachePutResult(ProceedingJoinPoint joinPoint) throws Throwable {// 可以在这里对参数也进行保护Object result = joinPoint.proceed();return deepCopyResult(result);}private Object deepCopyResult(Object result) {if (result == null || isImmutableType(result.getClass())) {return result;}try {byte[] bytes = objectMapper.writeValueAsBytes(result);return objectMapper.readValue(bytes, result.getClass());} catch (Exception e) {log.warn("缓存返回值保护失败: {}", e.getMessage());return result;}}private boolean isImmutableType(Class<?> clazz) {return clazz.isPrimitive() || clazz == String.class ||Number.class.isAssignableFrom(clazz) ||clazz == Boolean.class ||clazz == Character.class ||clazz == LocalDateTime.class ||clazz == LocalDate.class;}
}

6. 缓存配置最佳实践

6.1 安全缓存配置

# application.yml
spring:cache:type: caffeinecaffeine:spec: maximumSize=1000,expireAfterWrite=1h# 自定义配置
app:cache:protection:enabled: truedeep-copy: true

6.2 缓存配置类

@Configuration
@EnableCaching
@EnableAspectJAutoProxy
@Slf4j
public class CacheConfig {@Value("${app.cache.protection.enabled:true}")private boolean cacheProtectionEnabled;@Beanpublic CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(Duration.ofHours(1)).maximumSize(1000).recordStats());if (cacheProtectionEnabled) {log.info("启用缓存保护模式");return new ProtectiveCacheManager(cacheManager);}return cacheManager;}@Bean@ConditionalOnProperty(name = "app.cache.protection.enabled", havingValue = "true")public CacheProtectionAspect cacheProtectionAspect() {return new CacheProtectionAspect();}
}

7. 测试与验证

7.1 缓存安全测试工具

@Component
public class CacheSafetyValidator {@Autowiredprivate CacheManager cacheManager;/*** 验证缓存安全性*/public <T> CacheSafetyReport validateCacheSafety(String cacheName, Object key,Class<T> valueType) {Cache cache = cacheManager.getCache(cacheName);if (cache == null) {return CacheSafetyReport.notFound(cacheName);}T originalValue = cache.get(key, valueType);if (originalValue == null) {return CacheSafetyReport.empty(cacheName, key);}// 获取两次,检查是否为同一对象T firstGet = cache.get(key, valueType);T secondGet = cache.get(key, valueType);boolean isSameReference = (firstGet == secondGet);boolean isSafe = !isSameReference;return CacheSafetyReport.builder().cacheName(cacheName).key(key).isSafe(isSafe).isSameReference(isSameReference).valueType(valueType.getSimpleName()).build();}@Data@Builderpublic static class CacheSafetyReport {private String cacheName;private Object key;private boolean isSafe;private boolean isSameReference;private String valueType;private String message;public static CacheSafetyReport notFound(String cacheName) {return CacheSafetyReport.builder().cacheName(cacheName).isSafe(false).message("缓存不存在").build();}public static CacheSafetyReport empty(String cacheName, Object key) {return CacheSafetyReport.builder().cacheName(cacheName).key(key).isSafe(false).message("缓存值为空").build();}}
}

8. 总结与建议

8.1 核心要点

  1. 本地缓存存在对象引用共享风险
  2. @Cacheable不会自动防止对象修改
  3. 防御性拷贝是最可靠的保护方案
  4. 不可变对象是理想的缓存数据类型

8.2 选择策略

场景推荐方案说明
高性能要求手动深拷贝控制精细,性能最佳
开发效率序列化深拷贝实现简单,适用多数场景
安全关键不可变对象最高安全性,推荐使用
现有系统AOP保护无侵入式改造

8.3 强制规范

  1. 所有缓存返回的可变对象必须进行保护
  2. 新项目优先使用不可变对象
  3. 缓存配置必须包含安全保护机制
  4. 定期进行缓存安全性验证

通过遵循本指南,可以确保在使用Spring Boot本地缓存时,既享受其性能优势,又避免潜在的数据安全风险。

补充

上面关于本地缓存的关键特性说明中关于
不会反写数据:修改返回对象不会自动更新缓存
引用共享风险:本地缓存返回的是对象引用,而非副本

两点怎么理解?

核心比喻:图书馆与借书

假设缓存就像一个图书馆,缓存中的对象就像图书馆里收藏的书

  • :调用 @Cacheable 方法的应用程序
  • 借书:调用 getUserById(1) 从缓存获取数据
  • 图书管理员@Cacheable 注解的缓存机制

特性一:“不会反写数据”:修改返回对象不会自动更新缓存

比喻解释:

你从图书馆借了一本《三体》,回家后在书上乱涂乱画、撕掉了几页。这个过程,图书馆并不知道,也不会自动用你涂改后的书去替换馆里原来的那本。 第二天,另一个人来借《三体》,他拿到的还是图书馆书架上那本原始的、干净的书。

这里的 “反写” 指的是:你修改了借出的对象后,系统不会自动将这个修改后的对象同步回缓存。

代码验证:
@Service
public class UserService {@Cacheable(value = "users", key = "#id")public User getUserById(Long id) {System.out.println("执行数据库查询...");return new User(id, "原始名字", "原始邮箱");}
}// 测试代码
public void testNoBackWrite() {// 第一次调用:缓存没有,执行方法,存入缓存User user1 = userService.getUserById(1L); // 控制台输出:"执行数据库查询..."// 此时缓存中:{1: User(1, "原始名字", "原始邮箱")}// 修改对象的属性user1.setName("修改后的名字");user1.setEmail("修改后的邮箱");// 注意:此时缓存并不知道这个修改发生了!// 第二次调用:缓存命中,直接返回缓存中的对象User user2 = userService.getUserById(1L);// 控制台无输出(方法未执行)// 验证:返回的是原始缓存对象,不是我们修改后的对象System.out.println(user2.getName());  // 输出:"原始名字"System.out.println(user2.getEmail()); // 输出:"原始邮箱"
}

关键点@Cacheable 只在缓存未命中时写入,之后对返回值的任何修改都不会触发缓存的更新。


特性二:“引用共享风险”:本地缓存返回的是对象引用,而非副本

比喻解释(续接上文):

现在假设图书馆有个奇怪的规则:所有借书者拿到的都不是书的副本,而是图书馆藏书本身(即对象引用)。

  • 你借走了《三体》(获取对象引用)
  • 你在书上涂画(修改对象属性)
  • 因为另一个人借到的就是同一本实体书,所以他看到的是被涂画过的书
代码验证:
public void testReferenceSharingRisk() {// 第一次调用:创建对象并存入缓存User user1 = userService.getUserById(1L);// 缓存中:{1: User(1, "原始名字", "原始邮箱")}// user1 指向缓存中的那个User对象// 第二次调用:获取缓存中的对象User user2 = userService.getUserById(1L);// user2 也指向缓存中的同一个User对象// 验证引用相同System.out.println(user1 == user2); // 输出:true// 证明两个变量指向内存中的同一个对象// 危险操作开始!user1.setName("被污染的名字");// 第三次调用User user3 = userService.getUserById(1L);// 验证缓存已被污染System.out.println(user3.getName()); // 输出:"被污染的名字"System.out.println(user1 == user3);  // 输出:true
}

两个特性的关系:看似矛盾,实则统一

特性描述影响
不会反写数据你修改对象后,系统不会自动更新缓存表面上的"安全":你以为修改不影响缓存
引用共享风险你拿到的就是缓存中的对象本身实际上的"危险":你直接修改了缓存中的对象

矛盾的统一

  • 缓存机制的角度看:它确实"不会反写",因为根本没有触发写操作
  • 内存模型的角度看:你通过拿到的引用直接修改了缓存中的对象,相当于"绕过"了写机制

总结理解

  1. “不会反写数据” 说的是缓存系统的行为

    • 缓存系统不会监控你对返回对象做了什么
    • 没有自动的 cache.put() 被触发
  2. “引用共享风险” 说的是Java对象的内存模型

    • 本地缓存存储的是对象的内存地址
    • 你拿到这个地址后,可以直接修改那块内存的内容
    • 所有后续的获取者都会看到被修改后的内容

简单来说:你没有"更新"缓存,但你"污染"了缓存。

现实中的危险场景

// 在业务代码中
User user = userService.getUserById(1L); // 从缓存获取
user.setStatus("DISABLED"); // 业务逻辑:临时禁用// ... 其他代码// 另一处代码,或者其他线程
User sameUser = userService.getUserById(1L); 
// 此时拿到的user的status已经是"DISABLED"了,但数据库里还是正常状态!
// 产生了数据不一致

这就是为什么在本地缓存中必须使用防御性拷贝不可变对象的原因。

http://www.dtcms.com/a/491373.html

相关文章:

  • 我的远程开发革命:从环境配置噩梦到一键共享的蜕变
  • PVZ植物大战僵尸全集版分享下载 原版民间修改版含安卓手机+电脑+ios各平台
  • 网站建设公司专业公司排名wordpress 活动报名
  • 网站建设预付网站后台制作教程
  • 免费游戏网站制作化妆品做网站流程
  • 系统架构设计师备考第43天——软件架构演化和定义
  • 【Java笔记】消息队列
  • 网络监控工具:ping、traceroute、nmap、Wireshark 网络探测与分析
  • sward安装与配置,3分钟即可完成
  • 佛山网站建设方案me微擎怎么做网站
  • 影响网站访问速度网站开发所需经费
  • HTML5基础——7、CSS选择器
  • 千岛湖建设集团有限公司网站推广哪个网站好
  • 临清网站推广在哪里制作网页
  • 东莞微信网站建设报价建网络商城网站
  • PandaWiki:AI 驱动的开源知识库系
  • 中国旅游网站建设镇江网站排名优化费用
  • 今天遇到的一台爱普生L3258彩色喷墨打印机连续打印五灯齐闪故障的维修
  • 贵州建设水利厅考试网站购物网站的建设时间
  • DNS记录全解析:从A到MX
  • 使用OpenAI API和Python构建你的AI助手
  • Spring Boot入门指南:极速上手开发
  • 中电金信:从AI赋能到AI原生——企业级工具链平台重塑与建设实践
  • jsp 响应式网站模板两个网站开发swot分析
  • 建行个人网上银行登录入口亚马逊关键词快速优化
  • 做电视的视频网站wordpress windows下载
  • Pandas CSV:高效数据处理的利器
  • Kubernetes 存储核心理论:深入理解 PVC 静态迁移与动态扩容
  • 语言与文化差异如何影响国际化团队
  • 基于dtw算法的动作、动态识别