精通 Spring Cache + Redis:避坑指南与最佳实践
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 {@Beanpublic 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 和 publicobjectMapper.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 方法。
- @CachePut 执行,更新 Redis 缓存。
- @Transactional 开始事务。
- 方法体执行,更新数据库。
- 如果此时数据库更新失败,事务回滚。
- 结果: 数据库回滚了,但 Redis 缓存已经被更新为“新”数据,导致数据不一致(脏数据)。
最佳实践:分离关注点或延迟操作
- 分离方法 (推荐): 将数据库操作放在一个纯粹的 @Transactional 方法中,然后在调用该方法的外部、非事务方法中处理缓存更新/清除逻辑。
@Servicepublic class UserFacade { // 无事务@Autowiredprivate 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);}}@Servicepublic class UserServiceImpl implements UserService { // 纯事务@Transactionalpublic User updateUserInTransaction(User user) {// ... 更新数据库 ...// if (someError) throw new RuntimeException("DB update failed");return user;}@Transactionalpublic 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 自身注入到自己中,然后通过注入的实例来调用目标方法。
@Servicepublic class MyService {@Autowiredprivate 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 代理限制 这些关键点,并遵循相应的最佳实践,将帮助你构建出更加健壮、高效、易于维护的缓存系统。希望这篇避坑指南能让你在未来的开发中更加得心应手!
相关文章:
精通 Spring Cache + Redis:避坑指南与最佳实践
Spring Cache 以其优雅的注解方式,极大地简化了 Java 应用中缓存逻辑的实现。结合高性能的内存数据库 Redis,我们可以轻松构建出响应迅速、扩展性强的应用程序。然而,在享受便捷的同时,一些常见的“坑”和被忽视的最佳实践可能会悄…...
[SpringBoot]快速入门搭建springboot
默认有spring基础,不会一行代码一行代码那么细致地讲。 SpringBoot的作用 Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的。就像我们整个SSM框架时,就常常会碰到版本导致包名对不上、Bean非法参数类型的一系列问题(原出…...
理解.NET Core中的配置Configuration
什么是配置 .NET中的配置,本质上就是key-value键值对,并且key和value都是字符串类型。 在.NET中提供了多种配置提供程序来对不同的配置进行读取、写入、重载等操作,这里我们以为.NET 的源码项目为例,来看下.NET中的配置主要是有…...
C++面试八股文:智能指针
一、了解哪些智能指针? 回答:智能指针是用于管理动态分配的内存,行为类似于指针,但又具有自动管理内存的能力,所以称为智能指针。 首先说一下 auto_ptr和unique_ptr,它们都是独占式指针,同一时…...
nohup命令使用说明
文章目录 如何在后台运行程序呢?如何正常运行代码重定向呢?nohup: ignoring input 如何在后台运行程序呢? 使用nohup命令即可, nohup python dataset/ReferESpatialDataset.py >>dataset_20250417.log 2>&1 &n…...
MYSQL “Too Many Connections“ 错误解决
1.查询当前连接数 show status like "Threads_connected"; 2.查询数据库最大连接数 show variables like "max_connections" 3.查询所有活动连接 show processlist; 4.根据查询结果观察是否有长时间未被释放的连接 参数解释 : 字段说明id连接的唯一…...
Linux `init 6` 相关命令的完整使用指南
Linux init 6 相关命令的完整使用指南—目录 一、init 系统简介二、init 6 的含义与作用三、不同 Init 系统下的 init 6 行为1. SysVinit(如 CentOS 6、Debian 7)2. systemd(如 CentOS 7、Ubuntu 16.04)3. Upstart(如 …...
【外研在线-注册/登录安全分析报告】
前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…...
【NLP 63、大模型应用 —— Agent】
人与人最大的差距就是勇气和执行力,也是唯一的差距 —— 25.4.16 一、Agent 相关工作 二、Agent 特点 核心特征: 1.专有场景(针对某个垂直领域) 2.保留记忆(以一个特定顺序做一些特定任务,记忆当前任务的前…...
React 打包
路由懒加载 原本的加载方式 #使用lazy()函数声明的路由页面 使用Suspense组件进行加载 使用CDN优化...
2025.4.14-2025.4.20学习周报
目录 摘要Abstract1. 文献阅读1.1 模型架构1.2 实验分析1.3 代码实践 总结 摘要 在本周阅读的论文中,作者提出了一种名为MGSFformer的空气质量预测模型。模型通过残差去冗余模块可以有效解耦多粒度数据间的信息重叠;时空注意力模块采用并行建模策略&…...
Spring 微服务解决了单体架构的哪些痛点?
1. 部署困难 (Deployment Difficulty & Risk) 单体痛点: 整体部署: 对单体应用的任何微小修改(哪怕只是一行代码),都需要重新构建、测试和部署整个庞大的应用程序。部署频率低: 由于部署过程复杂且风险高,发布周期通常很长&a…...
【1】云原生,kubernetes 与 Docker 的关系
Kubernetes?K8s? Kubernetes经常被写作K8s。其中的数字8替代了K和s中的8个字母——这一点倒是方便了发推,也方便了像我这样懒惰的人。 什么是云原生? 云原生: 它是一种构建和运行应用程序的方法,它包含&am…...
Kubernetes控制平面组件:APIServer 限流机制详解
云原生学习路线导航页(持续更新中) kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计(一)Kubernetes架构原则和对象设计(二)Kubernetes架构原则和对象设计(三)Kubernetes控…...
springboot全局异常捕获处理
一、需求 实际项目中,经常抛出各种异常,不能直接抛出异常给前端,这样用户体验相当不好,用户看不懂你的Exception,对于一些sql异常,直接抛到页面上也不安全。所以有没有好的办法解决这些问题呢,当然有了&am…...
Flask(1): 在windows系统上部署项目1
1 前言 学习python也有段时间了,最近一个小项目要部署,正好把过程写下来。 在程序的结构上我选择了w/s模式,相比于c/s模式,无需考虑客户端的升级;框架我选择了flask,就是冲着轻量级去的,就是插件…...
【文献阅读】EndoNet A Deep Architecture for Recognition Tasks on Laparoscopic Videos
关于数据集的整理 Cholec80 胆囊切除手术视频数据集介绍 https://zhuanlan.zhihu.com/p/700024359 数据集信息 Cholec80 数据集 是一个针对内窥镜引导 下的胆囊切除手术视频流程识别数据集。数据集提供了每段视频中总共7种手术动作及总共7种手术工具的标注,标…...
基于springboot的个人财务管理系统的设计与实现
博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言࿰…...
Linux系统编程---孤儿进程与僵尸进程
1、前言 在上一篇博客文章已经对Linux系统编程内容进行了较为详细的梳理,本文将在上一篇的基础上,继续梳理Linux系统编程中关于孤儿进程和僵尸进程的知识脉络。如有疑问的博客朋友可以通过下面的博文链接进行参考学习。 Linux系统编程---多进程-CSDN博客…...
简单使用MCP
简单使用MCP 1 简介 模型上下文协议(Model Context Protocol,MCP)是由Anthropic(产品是Claude)推出的开放协议,它规范了应用程序如何向LLM提供上下文。MCP可帮助你在LLM之上构建代理和复杂的工作流。 从…...
Semaphore的核心机制
在 Java 中,Semaphore 通过 许可计数器 和 同步队列 的机制实现并发线程数的限制。以下是其核心实现原理和步骤的详细分析: 一、核心机制 许可计数器(Permits) • 初始化时指定的许可数(如 new Semaphore(3)࿰…...
计算机视觉与深度学习 | RNN原理,公式,代码,应用
RNN(循环神经网络)详解 一、原理 RNN(Recurrent Neural Network)是一种处理序列数据的神经网络,其核心思想是通过循环连接(隐藏状态)捕捉序列中的时序信息。每个时间步的隐藏状态 ( h_t ) 不仅依赖当前输入 ( x_t ),还依赖前一时间步的隐藏状态 ( h_{t-1} ),从而实现…...
Keil MDK 编译问题:last line of file ends without a newline
问题与处理策略 问题描述 ..\..\User\main.c(38): warning: #1-D: last line of file ends without a newline} ..\..\User\main.c: 1 warning, 0 errors问题原因 这是文件末尾缺少换行符警告 处理策略 在文件(main.c)的最后一行按回车键添加一个空…...
MySQL:9.表的内连和外连
9.表的内连和外连 表的连接分为内连和外连 9.1 内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选,之前查询都是内连 接,也是在开发过程中使用的最多的连接查询。 语法: select 字段 from 表1 inner join 表2 on 连接…...
C++栈操作集合
数组 #include <bits/stdc.h> using namespace std;class sss{ private:int a[1000];int curr -1; public:void push(int);void pop();int top();bool empyt();int size(); };int main() {sss n;while(true){int a;cout<<"1.添加\n2.删除-\n3.显示栈顶\n4.储…...
在阿里云和树莓派上编写一个守护进程程序
目录 一、阿里云邮件守护进程 1. 安装必要库 2. 创建邮件发送脚本 mail_daemon.py 3. 设置后台运行 二、树莓派串口守护进程 1. 启用树莓派串口 2. 安装依赖库 3. 创建串口输出脚本 serial_daemon.py 4. 设置开机自启 5. 使用串口助手接收 一、阿里云邮件守护进程 1.…...
每日一题——最小测试用例集覆盖问题
最小测试用例集覆盖问题(C语言实现) 问题描述 假设我们有一系列测试用例,每个测试用例会覆盖若干个代码模块。 我们使用一个二维数组来表示这些测试用例的覆盖情况: 如果某个测试用例 i 能覆盖代码模块 j,则数组中…...
LangChain 单智能体模式示例【纯代码】
# LangChain 单智能体模式示例import os from typing import Anyfrom langchain.agents import AgentType, initialize_agent, Tool from langchain_openai import ChatOpenAI from langchain.tools import BaseTool from langchain_experimental.tools.python.tool import Pyt…...
基于前端技术的QR码API开发实战:从原理到部署
前言 QR码(Quick Response Code)是一种二维码,于1994年开发。它能快速存储和识别数据,包含黑白方块图案,常用于扫描获取信息。QR码具有高容错性和快速读取的优点,广泛应用于广告、支付、物流等领域。通过扫…...
RenderStage::drawInner
文章目录 RenderStage::drawInnerOSG渲染后台关系图OSG的渲染流程RenderBin::draw(renderInfo,previous)RenderBin::drawImplementationRenderLeaf::renderosg::State::apply(const StateSet*)Drawable::draw(RenderInfo& renderInfo)Drawable::drawInner(RenderInfo& …...
