精通 Spring Cache + Redis:避坑指南与最佳实践

news2025/4/21 15:48:36

Spring Cache 以其优雅的注解方式,极大地简化了 Java 应用中缓存逻辑的实现。结合高性能的内存数据库 Redis,我们可以轻松构建出响应迅速、扩展性强的应用程序。然而,在享受便捷的同时,一些常见的“坑”和被忽视的最佳实践可能会悄悄地影响你的应用性能和稳定性。

本文将深入探讨在使用 Spring Cache 结合 Redis 时最需要注意的几个关键点,并提供切实可行的避坑指南和最佳实践,助你用好

1. 序列化陷阱:告别乱码,拥抱 JSON

问题: 当你兴冲冲地配置好 Spring Cache 和 Redis,并缓存了一个 Java 对象后,去 Redis 里查看,可能会看到一堆类似 ¬í\x00\x05sr\x00\x0Ecom.example...​ 的乱码。这是因为 Spring Boot 默认使用了 JDK 的序列化机制 (JdkSerializationRedisSerializer​)。

痛点:

  • 可读性为零: 无法直观判断缓存内容,调试极其困难。
  • 跨语言障碍: Java 特有格式,其他语言服务无法读取。
  • 版本兼容性差: 类结构变更可能导致反序列化失败。
  • 潜在安全风险: 反序列化漏洞不容忽视。

最佳实践:使用 JSON 序列化 (Jackson)

JSON 格式是文本格式,具有良好的可读性和跨语言通用性。通过配置 Jackson2JsonRedisSerializer​,你可以让缓存在 Redis 中的数据变得清晰可见,例如 {"id":123,"name":"Alice","email":"alice@example.com"}​。

如何配置? 创建一个 RedisCacheConfiguration​ Bean:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching // 不要忘记开启缓存
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // 配置 JSON 序列化器
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = createJacksonSerializer();

        // 默认缓存配置:键用 String 序列化,值用 JSON 序列化,默认 TTL 1 小时
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)) // 设置默认 TTL
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSerializer));

        // 可以为特定的 Cache Name 配置不同的 TTL 等
        // Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        // cacheConfigurations.put("users", defaultCacheConfig.entryTtl(Duration.ofMinutes(30)));
        // cacheConfigurations.put("products", defaultCacheConfig.entryTtl(Duration.ofDays(1)));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(defaultCacheConfig)
                // .withInitialCacheConfigurations(cacheConfigurations) // 启用特定配置
                .build();
    }

    private Jackson2JsonRedisSerializer<Object> createJacksonSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 指定要序列化的域、getter/setter 以及修饰符范围,ANY 是包括 private 和 public
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非 final 修饰的。final 修饰的类,比如 String, Integer 等会抛出异常
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        // 解决 Jackson2 无法反序列化 LocalDateTime 的问题
        objectMapper.registerModule(new JavaTimeModule());

        return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    }
}

关键: 使用 JSON 序列化能显著提升开发和调试效率,强烈推荐!

2. Key 的艺术:规范命名,动态生成

问题: 缓存的 Key 设计混乱,或者过于简单,可能导致:

  • Key 冲突: 不同业务数据使用了相同的 Key,导致缓存覆盖或读取错误。
  • 难以理解和管理: 无法通过 Key 快速定位对应的业务数据。
  • 批量清除困难: 无法按模块或业务维度精确清理缓存。

最佳实践:规范化、层级化、动态化

  • 规范格式: 推荐使用 :​ 分隔的层级结构,例如 模块名:业务名:唯一标识符​。如 user:info:123​ 或 product:detail:sku1001​。
  • 利用 SpEL: Spring Cache 的 key​ 属性支持强大的 SpEL (Spring Expression Language),可以动态地根据方法参数生成 Key。
@Service
public class UserServiceImpl implements UserService {

    // 使用 SpEL 引用方法参数 id,并结合固定前缀
    @Cacheable(value = "user:info", key = "#id")
    public User getUserById(Long id) {
        // ... 查询数据库 ...
        return user;
    }

    // 使用 SpEL 引用对象参数的属性
    @CachePut(value = "user:info", key = "#user.id")
    public User updateUser(User user) {
        // ... 更新数据库 ...
        return user;
    }

    // 引用第一个参数 (p0) 和第二个参数的 email 属性
    @Cacheable(value = "user:auth", key = "#p0 + ':' + #p1.email")
    public String getUserToken(Long userId, LoginRequest request) {
        // ... 生成 Token ...
        return token;
    }

    @CacheEvict(value = "user:info", key = "#id")
    public void deleteUser(Long id) {
        // ... 删除数据库 ...
    }
}

关键: 设计良好、一致的 Key 命名策略是高效使用缓存的基础。

3. TTL 的守护:设置过期时间,防止内存溢出

问题: 不设置缓存过期时间 (Time-To-Live, TTL),数据将永久存储在 Redis 中,直到手动删除或 Redis 内存耗尽。这会导致:

  • 内存溢出风险: Redis 内存持续增长,最终可能导致服务崩溃。
  • 数据不一致: 数据库数据已更新,但缓存仍然是旧数据(脏数据)。

最佳实践:合理配置 TTL

  • 全局默认 TTL: 在 RedisCacheConfiguration​ 中设置一个全局的默认过期时间 (entryTtl​),作为基础保障。 (见上面配置示例)
  • 特定 Cache Name 的 TTL: 可以为不同的 cacheNames​ (通过 @Cacheable​ 的 value​ 或 cacheNames​ 属性指定) 配置不同的 TTL。例如,用户会话缓存可能只需要 30 分钟,而商品信息缓存可以设置为 1 天。 (见上面配置示例中的注释部分)
  • 评估数据变更频率: TTL 的设置需要权衡:TTL 太短,缓存命中率低;TTL 太长,数据一致性风险高。需要根据业务数据的实际更新频率来决定。

关键: 永远不要忘记为你的缓存设置一个合理的过期时间!

4. 事务的纠缠:@Transactional​ 与缓存注解的顺序迷思

问题: 当 @CachePut​ 或 @CacheEvict​ 与 @Transactional​ 用在同一个方法上时,可能会出现问题。因为 Spring Cache 的 AOP 拦截器通常在事务 AOP 拦截器之前执行。

场景: 一个带有 @Transactional​ 和 @CachePut​ 的 updateUser​ 方法。

  1. ​@CachePut​ 执行,更新 Redis 缓存。
  2. ​@Transactional​ 开始事务。
  3. 方法体执行,更新数据库。
  4. 如果此时数据库更新失败,事务回滚。
  5. 结果: 数据库回滚了,但 Redis 缓存已经被更新为“新”数据,导致数据不一致(脏数据)。

最佳实践:分离关注点或延迟操作

  • 分离方法 (推荐): 将数据库操作放在一个纯粹的 @Transactional​ 方法中,然后在调用该方法的外部、非事务方法中处理缓存更新/清除逻辑。
   @Service
   public class UserFacade { // 无事务

       @Autowired
       private UserService userService; // 包含事务方法

       @CachePut(value = "user:info", key = "#user.id") // 缓存操作在事务外部
       public User updateUserAndCache(User user) {
           return userService.updateUserInTransaction(user); // 调用事务方法
       }

       @CacheEvict(value = "user:info", key = "#id")
       public void deleteUserAndEvictCache(Long id) {
           userService.deleteUserInTransaction(id);
       }
   }

   @Service
   public class UserServiceImpl implements UserService { // 纯事务

       @Transactional
       public User updateUserInTransaction(User user) {
           // ... 更新数据库 ...
           // if (someError) throw new RuntimeException("DB update failed");
           return user;
       }

       @Transactional
       public void deleteUserInTransaction(Long id) {
           // ... 删除数据库 ...
       }
   }
  • 事务同步管理器 (较复杂): 使用 TransactionSynchronizationManager.registerSynchronization​ 注册一个回调,在事务成功提交后才执行缓存操作。这需要更复杂的编码。

关键: 尽量避免在同一个方法上混合 @Transactional​ 和写操作的缓存注解 (@CachePut​, @CacheEvict​)。优先选择分离方法。

5. AOP 的限制:内部调用失效之谜

问题: 在同一个 Service 类中,一个没有缓存注解的方法 A 调用了同一个类中带有 @Cacheable​ 的方法 B,你会发现方法 B 的缓存逻辑没有生效。

@Service
public class MyService {

    @Cacheable("myCache")
    public String cachedMethod(String key) {
        System.out.println("Executing cachedMethod for key: " + key);
        return "Data for " + key;
    }

    public String callingMethod(String key) {
        System.out.println("Calling cachedMethod internally...");
        // !!! 内部调用,cachedMethod 的缓存注解会失效 !!!
        return this.cachedMethod(key);
    }
}

原因: Spring AOP (包括缓存) 是通过代理实现的。外部调用 Service Bean 的方法时,访问的是代理对象,代理对象会执行缓存等切面逻辑。但是,当 Bean 的一个方法直接调用同一个 Bean 的另一个方法时 (this.methodB()​),它绕过了代理,直接调用了原始对象的方法,导致 AOP 切面(缓存注解)失效。

最佳实践:通过代理调用

  • 注入自身 (常用): 将 Service 自身注入到自己中,然后通过注入的实例来调用目标方法。
   @Service
   public class MyService {

       @Autowired
       private MyService self; // 注入自身代理

       @Cacheable("myCache")
       public String cachedMethod(String key) {
           System.out.println("Executing cachedMethod for key: " + key);
           return "Data for " + key;
       }

       public String callingMethod(String key) {
           System.out.println("Calling cachedMethod via self-proxy...");
           // 通过代理调用,缓存注解会生效
           return self.cachedMethod(key);
       }
   }

注意: 可能需要配置 Spring 允许循环依赖(虽然在新版本 Spring Boot 中,对于单例 Bean 的 Autowired​ 注入通常是允许的)。

  • 移到另一个 Bean (更清晰): 将需要被缓存的方法 (cachedMethod​) 移到另一个独立的 Bean 中,然后在 MyService​ 中注入并调用这个新的 Bean。这是更推荐的解耦方式。

关键: 理解 Spring AOP 代理机制是解决内部调用失效问题的关键。

总结

Spring Cache 与 Redis 的结合为 Java 应用带来了巨大的性能优势和开发便利。然而,魔鬼藏在细节中。关注 序列化选择、Key 的设计、TTL 的设置、事务交互 以及 AOP 代理限制 这些关键点,并遵循相应的最佳实践,将帮助你构建出更加健壮、高效、易于维护的缓存系统。希望这篇避坑指南能让你在未来的开发中更加得心应手!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2339525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[SpringBoot]快速入门搭建springboot

默认有spring基础&#xff0c;不会一行代码一行代码那么细致地讲。 SpringBoot的作用 Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的。就像我们整个SSM框架时&#xff0c;就常常会碰到版本导致包名对不上、Bean非法参数类型的一系列问题&#xff08;原出…

理解.NET Core中的配置Configuration

什么是配置 .NET中的配置&#xff0c;本质上就是key-value键值对&#xff0c;并且key和value都是字符串类型。 在.NET中提供了多种配置提供程序来对不同的配置进行读取、写入、重载等操作&#xff0c;这里我们以为.NET 的源码项目为例&#xff0c;来看下.NET中的配置主要是有…

MYSQL “Too Many Connections“ 错误解决

1.查询当前连接数 show status like "Threads_connected"; 2.查询数据库最大连接数 show variables like "max_connections" 3.查询所有活动连接 show processlist; 4.根据查询结果观察是否有长时间未被释放的连接 参数解释 : 字段说明id连接的唯一…

【外研在线-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

【NLP 63、大模型应用 —— Agent】

人与人最大的差距就是勇气和执行力&#xff0c;也是唯一的差距 —— 25.4.16 一、Agent 相关工作 二、Agent 特点 核心特征&#xff1a; 1.专有场景&#xff08;针对某个垂直领域&#xff09; 2.保留记忆&#xff08;以一个特定顺序做一些特定任务&#xff0c;记忆当前任务的前…

React 打包

路由懒加载 原本的加载方式 #使用lazy()函数声明的路由页面 使用Suspense组件进行加载 使用CDN优化

2025.4.14-2025.4.20学习周报

目录 摘要Abstract1. 文献阅读1.1 模型架构1.2 实验分析1.3 代码实践 总结 摘要 在本周阅读的论文中&#xff0c;作者提出了一种名为MGSFformer的空气质量预测模型。模型通过残差去冗余模块可以有效解耦多粒度数据间的信息重叠&#xff1b;时空注意力模块采用并行建模策略&…

【1】云原生,kubernetes 与 Docker 的关系

Kubernetes&#xff1f;K8s&#xff1f; Kubernetes经常被写作K8s。其中的数字8替代了K和s中的8个字母——这一点倒是方便了发推&#xff0c;也方便了像我这样懒惰的人。 什么是云原生&#xff1f; 云原生&#xff1a; 它是一种构建和运行应用程序的方法&#xff0c;它包含&am…

Kubernetes控制平面组件:APIServer 限流机制详解

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…

springboot全局异常捕获处理

一、需求 实际项目中&#xff0c;经常抛出各种异常&#xff0c;不能直接抛出异常给前端&#xff0c;这样用户体验相当不好&#xff0c;用户看不懂你的Exception,对于一些sql异常&#xff0c;直接抛到页面上也不安全。所以有没有好的办法解决这些问题呢&#xff0c;当然有了&am…

【文献阅读】EndoNet A Deep Architecture for Recognition Tasks on Laparoscopic Videos

关于数据集的整理 Cholec80 胆囊切除手术视频数据集介绍 https://zhuanlan.zhihu.com/p/700024359 数据集信息 Cholec80 数据集 是一个针对内窥镜引导 下的胆囊切除手术视频流程识别数据集。数据集提供了每段视频中总共7种手术动作及总共7种手术工具的标注&#xff0c;标…

基于springboot的个人财务管理系统的设计与实现

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

Linux系统编程---孤儿进程与僵尸进程

1、前言 在上一篇博客文章已经对Linux系统编程内容进行了较为详细的梳理&#xff0c;本文将在上一篇的基础上&#xff0c;继续梳理Linux系统编程中关于孤儿进程和僵尸进程的知识脉络。如有疑问的博客朋友可以通过下面的博文链接进行参考学习。 Linux系统编程---多进程-CSDN博客…

简单使用MCP

简单使用MCP 1 简介 模型上下文协议&#xff08;Model Context Protocol&#xff0c;MCP&#xff09;是由Anthropic&#xff08;产品是Claude&#xff09;推出的开放协议&#xff0c;它规范了应用程序如何向LLM提供上下文。MCP可帮助你在LLM之上构建代理和复杂的工作流。 从…

MySQL:9.表的内连和外连

9.表的内连和外连 表的连接分为内连和外连 9.1 内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选&#xff0c;之前查询都是内连 接&#xff0c;也是在开发过程中使用的最多的连接查询。 语法&#xff1a; select 字段 from 表1 inner join 表2 on 连接…

在阿里云和树莓派上编写一个守护进程程序

目录 一、阿里云邮件守护进程 1. 安装必要库 2. 创建邮件发送脚本 mail_daemon.py 3. 设置后台运行 二、树莓派串口守护进程 1. 启用树莓派串口 2. 安装依赖库 3. 创建串口输出脚本 serial_daemon.py 4. 设置开机自启 5. 使用串口助手接收 一、阿里云邮件守护进程 1.…

基于前端技术的QR码API开发实战:从原理到部署

前言 QR码&#xff08;Quick Response Code&#xff09;是一种二维码&#xff0c;于1994年开发。它能快速存储和识别数据&#xff0c;包含黑白方块图案&#xff0c;常用于扫描获取信息。QR码具有高容错性和快速读取的优点&#xff0c;广泛应用于广告、支付、物流等领域。通过扫…

RenderStage::drawInner

文章目录 RenderStage::drawInnerOSG渲染后台关系图OSG的渲染流程RenderBin::draw(renderInfo,previous)RenderBin::drawImplementationRenderLeaf::renderosg::State::apply(const StateSet*)Drawable::draw(RenderInfo& renderInfo)Drawable::drawInner(RenderInfo& …

C++初阶-类和对象(中)

目录 1.类的默认成员函数 2.构造函数&#xff08;难度较高&#xff09; ​编辑 ​编辑 ​编辑 3.析构函数 4.拷贝构造函数 5.赋值运算符重载 5.1运算符重载 5.2赋值运算符重载 6.取地址运算符重载 6.1const成员函数 6.2取地址运算符重载 7.总结 1.类的默认成员函数…

智谱开源新一代GLM模型,全面布局AI智能体生态

2024年4月15日&#xff0c;智谱在中关村论坛上正式发布了全球首个集深度研究与实际操作能力于一体的AI智能体——AutoGLM沉思。这一革命性技术的发布标志着智谱在AGI&#xff08;通用人工智能&#xff09;领域的又一次重要突破。智谱的最新模型不仅推动了AI智能体技术的升级&am…