导读:本期聚焦于缅甸程序员创作的《如何在Java Spring中使用Redis实现高效缓存?Spring Cache抽象详解》,敬请观看详情。为什么同样的接口有人响应几十毫秒,有人却要几百毫秒?缓存层的引入往往是最直接的优化手段。Spring Cache提供了一套与具体存储无关的缓存抽象,配合Redis可以做到注解驱动、无侵入的业务缓存。本文围绕Spring Cache的核心注解Cacheable、CachePut与CacheEvict展开,讲清SpEL键生成规则、RedisCacheManager的自定义配置、序列化方案选择以及TTL过期策略,同时分析注解失效的常见陷阱与二级缓存思路,帮助你在实际项目中把缓存用稳用好。

缓存是提升系统吞吐量最直接的手段之一,而Redis凭借内存存储和丰富的数据结构,几乎成了Java后端缓存的默认选择。Spring框架从3.1开始提供了统一的缓存抽象层,也就是Spring Cache,它把缓存的读写逻辑从业务代码中剥离出来,开发者只需要在方法上加几个注解,就能完成缓存的查询、更新和清除。这篇文章将围绕Redis与Spring Cache的整合展开,从基本用法讲到配置细节,再到常见的踩坑点,帮你把缓存这套机制真正用明白。

如何在Java Spring中使用Redis实现高效缓存?Spring Cache抽象详解

一、Spring Cache抽象的核心概念与注解

Spring Cache的设计思路与Spring对事务的管理类似,都是基于AOP代理实现的。当Spring容器启动时,会对标注了缓存注解的Bean生成代理对象,方法调用先经过缓存拦截器,命中缓存则直接返回,未命中才执行真实方法并把结果写入缓存。这个机制决定了它对业务代码几乎零侵入,你不需要在Service里手写RedisTemplate的get和set逻辑。

核心注解主要有三个:@Cacheable负责查询缓存,方法执行前先查,命中则跳过方法体;@CachePut每次都会执行方法并把返回值刷新到缓存,适合更新场景;@CacheEvict用于删除缓存,支持按key删除和全量清空。此外还有@Caching用于组合多个注解,以及@CacheConfig用于在类级别统一声明缓存名称。看一个典型例子:

@Service
public class UserService {

    // 先查user缓存,key为参数id,未命中则执行方法并缓存结果
    @Cacheable(value = "user", key = "#id")
    public User getUserById(Long id) {
        System.out.println("从数据库查询用户: " + id);
        return userMapper.selectById(id);
    }

    // 更新用户并刷新缓存
    @CachePut(value = "user", key = "#user.id")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user;
    }

    // 删除用户时清除对应缓存
    @CacheEvict(value = "user", key = "#id")
    public void deleteUser(Long id) {
        userMapper.deleteById(id);
    }
}

key属性支持SpEL表达式,这是Spring Cache最灵活的地方。#id表示方法参数,#result表示返回值(仅在@CachePut@CacheEvict的beforeInvocation为false时可用),#p0#a0这类下标写法也支持。如果方法有多个参数,可以拼接成组合键,例如key = "#userId + ':' + #orderId"。当不指定key时,Spring会使用内置的SimpleKeyGenerator,把所有参数拼成一个key,无参方法则用SimpleKey.EMPTY表示。需要注意的是,无参方法加@Cacheable意味着所有调用共享同一个缓存值,这在实际业务中往往是错误设计,务必想清楚缓存粒度。

二、整合Redis:依赖引入与缓存管理器配置

要让Spring Cache的底层存储变成Redis,需要引入spring-boot-starter-data-redis和spring-boot-starter-cache两个依赖,然后在配置类上添加@EnableCaching开启缓存支持。此时Spring Boot会自动装配RedisCacheManager,缓存的key会被加上前缀(默认是缓存名加双冒号),value则默认使用JDK序列化。JDK序列化虽然能用,但生成的字节数组体积大、可读性差,还要求实体类实现Serializable接口,生产环境一般都会换成JSON序列化。

自定义RedisCacheManager的核心在于序列化器和TTL的设置。下面是一份常用配置:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 缓存key前缀,entryTtl设置默认过期时间30分钟
                .prefixCacheNameWith("app:")
                .entryTtl(Duration.ofMinutes(30))
                // 禁用缓存空值(也可选择开启以防缓存穿透)
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 针对不同缓存名设置差异化的过期时间
        Map<String, RedisCacheConfiguration> configs = new HashMap<>();
        configs.put("user", config.entryTtl(Duration.ofHours(2)));
        configs.put("hotData", config.entryTtl(Duration.ofMinutes(5)));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .withInitialCacheConfigurations(configs)
                .build();
    }
}

这份配置里有几个值得展开的点。第一,prefixCacheNameWith给所有key加了统一前缀,多项目共用一个Redis实例时可以避免key冲突。第二,entryTtl支持按缓存名分别设置TTL,热点数据可以用较短的过期时间保证新鲜度,相对稳定的数据则设置长一些减少回源。第三,GenericJackson2JsonRedisSerializer会在JSON中写入类型信息,反序列化时能还原成原始对象,这是它比普通的Jackson2JsonRedisSerializer(需要每个类型单独构造)更省事的地方,代价是JSON体积稍大。如果你的实体类结构频繁变动,类型信息还可能导致反序列化失败,这时要考虑版本兼容问题。

另外提醒一点,使用JSON序列化时如果实体的某些字段是LocalDateTime等Java 8时间类型,需要ObjectMapper注册JavaTimeModule,否则序列化会直接抛异常。这个坑非常常见,报错信息往往只是一句SerializationException,排查时别忘了检查时间字段。

三、注解失效的常见陷阱与最佳实践

Spring Cache基于AOP代理,这就带来了一个经典问题:类内部方法自调用会导致注解失效。比如同一个Service里有方法A调用标注了@Cacheable的方法B,调用A时走的是this引用而非代理对象,缓存拦截根本不会发生。解决办法通常有三种:把方法拆到另一个Bean中;注入自身代理(通过AopContext.currentProxy()并开启exposeProxy);或者干脆不依赖注解,改用RedisTemplate手动控制。实践中推荐第一种,结构也更清晰。

还有几类高频问题需要留意。其一是缓存的key设计不当导致读到脏数据,比如对象更新后缓存key没变但内容变了,务必保证key与业务唯一标识严格对应。其二是缓存穿透,查询一个数据库中不存在的id,每次都会穿透到数据库,可以通过.computeIfAbsent之外的方案,也就是开启空值缓存(去掉disableCachingNullValues并用unless = "#result == null"控制)或者引入布隆过滤器解决。其三是缓存雪崩,大量key同时过期会造成数据库瞬时压力,除了给TTL加随机偏移量,还可以结合多级缓存思路,本地Caffeine加远程Redis,短TTL放本地、长TTL放Redis。

对比一下Spring Cache注解方式和直接使用RedisTemplate的适用场景:注解方式适合读写模式固定、以整个方法返回值为缓存单元的场景,代码简洁、维护成本低;RedisTemplate适合需要精细操作Redis数据结构(比如Hash的局部更新、计数器、分布式锁)的场景,灵活但代码量大。两者并不互斥,实际项目中通常是主体查询走注解缓存,特殊逻辑手动操作,各自发挥优势。

最后总结一下:Spring Cache把缓存从业务代码中解耦出来,配合Redis可以低成本获得性能提升,但它的便利建立在理解其代理机制和key策略的基础上。配置好序列化器、设计好TTL、避开自调用陷阱,再针对穿透和雪崩做好防御,这套缓存体系就能稳定支撑高并发场景。如果你的项目还在用散落各处的RedisTemplate硬编码,不妨逐步迁移到注解缓存,代码的可读性和可维护性会有明显改善。

RedisSpring CacheJava缓存修改时间:2026-08-31 22:49:01

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。