Java 后端详解(五):Redis 缓存

25 阅读10分钟

上篇:Java 后端详解(四):分页与搜索
本篇以文章模块为例,讲清:Redis 如何接入 Spring BootSpring Cache 注解怎么用文章列表与详情如何缓存、何时失效


一、本篇要解决的问题

上一篇做完分页与搜索后,每次访问文章列表或详情都会查 MySQL。热门文章被反复打开、首页频繁刷新时,数据库压力会明显上升。

场景无缓存时有 Redis 缓存后
首页拉「最新 5 篇」每次 SELECT ... LIMIT 5命中缓存,直接读内存
文章详情页被多人浏览同一 id 重复查库第一次查库,后续读 Redis
同一页码反复翻页重复执行分页 SQLpage:size:keyword 缓存结果

改造目标:读多写少的接口走缓存,写操作后自动清掉旧数据,保证用户看到的是最新内容。


二、整体架构:请求经过哪几层

浏览器
  │
  ▼
ArticleController
  │
  ▼
ArticleService  ←── Spring AOP 在这里拦截 @Cacheable / @CacheEvict
  │
  ├── 缓存命中 ──→ Redis(articles / articlePages)
  │
  └── 缓存未命中 ──→ ArticleRepository ──→ MySQL

本项目采用 Spring Cache 抽象 + Redis 实现

  • 业务代码只写 @Cacheable@CacheEvict,不直接拼 Redis 命令
  • 底层由 RedisCacheManager 把缓存存进 Redis
  • 同时提供 RedisTemplate,方便以后做限流、分布式锁等扩展

三、第一步:引入依赖

backend/pom.xml 中增加两个 starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
依赖作用
spring-boot-starter-data-redisRedis 连接、序列化、RedisTemplate
spring-boot-starter-cache@Cacheable 等缓存注解与 AOP 支持

Spring Boot 3.x 默认使用 Lettuce 客户端连接 Redis,无需额外配置驱动。


四、第二步:application.yml 连接 Redis

spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 3000ms
  cache:
    type: redis
    redis:
      time-to-live: 1800000   # 毫秒,30 分钟
配置项含义
spring.data.redis.hostRedis 地址,本地开发一般为 localhost
spring.data.redis.port端口,默认 6379
spring.data.redis.timeout连接超时
spring.cache.type: redis告诉 Spring Cache 用 Redis 而不是本地 Map
spring.cache.redis.time-to-live全局默认过期时间(与代码里 RedisCacheConfiguration 可叠加理解,最终以 CacheManager Bean 为准)

注意:应用启动前 Redis 必须已运行,否则连接失败会导致启动报错。

本地启动 Redis 的常见方式:

环境命令 / 方式
Linux / WSLredis-server
Docker(本地)docker run -d -p 6379:6379 redis:7
Windows可使用 Memurai 等 Redis 兼容产品

五、第三步:RedisConfig 配置类

文件:backend/src/main/java/com/blog/config/RedisConfig.java

这个类做三件事,先用一张图记住:

application.yml          RedisConfig                    ArticleService
(host / port)                 │                              │
      │                       │                              │
      ▼                       ▼                              ▼
RedisConnectionFactory ──► cacheManager ◄──── @Cacheable / @CacheEvict  ← 文章缓存走这条
      │                       ▲
      │                  redisObjectMapper(JSON 序列化规则)
      │
      └──► redisTemplate(手动操作 Redis,本项目暂未使用)
Bean干什么本项目用没用
redisObjectMapper规定对象怎么转成 JSON 存进 Redis间接使用
cacheManager@Cacheable 注解能读写 Redis在用
redisTemplate代码里手写 get / set / increment暂未使用,预留扩展

5.1 类声明

@Configuration
@EnableCaching
public class RedisConfig {
  • @Configuration:配置类,里面的 @Bean 方法会注册到 Spring 容器
  • @EnableCaching必须加,否则 ArticleService 上的 @Cacheable 不会生效

5.2 redisObjectMapper():对象怎么存进 Redis

@Bean
public ObjectMapper redisObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY);
    return mapper;
}

Redis 里不能存 Java 对象,只能存字符串。这个 Bean 负责 Java 对象 ↔ JSON 的转换,主要解决两个问题:

LocalDateTime 怎么序列化

ArticlecreatedAt 字段。加上 JavaTimeModule 后,时间会存成 "2026-06-30T10:00:00",而不是报错或变成数组。

② 读回来的时候怎么知道是 Article

加上 activateDefaultTyping 后,JSON 里会多一个 @class 字段:

{
  "@class": "com.blog.entity.Article",
  "id": 1,
  "title": "Spring Boot 入门",
  "createdAt": "2026-06-30T10:00:00"
}

没有这个字段,读回来会变成 LinkedHashMaparticle.getTitle() 就会出问题。


5.3 redisTemplate():手动操作 Redis(预留)

@Bean
public RedisTemplate<String, Object> redisTemplate(
        RedisConnectionFactory connectionFactory,
        ObjectMapper redisObjectMapper) {
    GenericJackson2JsonRedisSerializer serializer =
            new GenericJackson2JsonRedisSerializer(redisObjectMapper);

    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    template.setKeySerializer(new StringRedisSerializer());       // key → 可读字符串
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(serializer);                      // value → JSON
    template.setHashValueSerializer(serializer);
    template.afterPropertiesSet();
    return template;
}

RedisTemplate 是底层 API,在代码里直接写 Redis 命令。上面几行配置的含义很简单:

  • key 用字符串:Redis 里看到 view:123,方便调试
  • value 用 JSON:和 5.2 的 redisObjectMapper 规则一致

适合计数、限流等场景,例如:

redisTemplate.opsForValue().increment("article:view:" + id);

当前项目的文章缓存不走它,走的是下面的 cacheManager + 注解。这里提前配好,以后扩展时可以直接 @Autowired 使用。


5.4 cacheManager():给 @Cacheable 用(核心)

@Bean
public RedisCacheManager cacheManager(
        RedisConnectionFactory connectionFactory,
        ObjectMapper redisObjectMapper) {
    GenericJackson2JsonRedisSerializer serializer =
            new GenericJackson2JsonRedisSerializer(redisObjectMapper);

    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))          // 30 分钟过期
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(serializer));

    return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
}

@Cacheable 本身不操作 Redis,真正干活的是这个 cacheManager。它规定了三件事:

配置效果
entryTtl(30分钟)缓存自动过期,防止脏数据一直留着
key 用字符串Redis 里看到 articles::1,方便调试
value 用 JSON和 5.2 的 redisObjectMapper 规则一致

和文章接口怎么配合?

操作注解Redis 做什么
getById 读详情@Cacheable先查 articles::id,没有则查库并写入
queryArticles 读列表@Cacheable先查 articlePages::页码:条数:关键词
create 新建@CacheEvict清空所有列表缓存(新文章还没 id,无详情缓存)
update / delete@CacheEvict删该文章详情 + 清空所有列表缓存

读走 cacheManager 查/写缓存,写只清缓存不写缓存——新文章要等第一次 getById 才会被缓存进去。


5.5 小结:两个出口,别搞混

读文章(getById / queryArticles)
  → @Cacheable → cacheManager → Redis 查/写

写文章(create / update / delete)
  → @CacheEvict → cacheManager → Redis 删缓存
  → articleRepository → MySQL 写库

以后做浏览量、限流
  → redisTemplate → Redis 直接操作

记住一句:本项目文章缓存只用 cacheManagerredisTemplate 是备用。


六、第四步:在 ArticleService 上加缓存注解

文件:backend/src/main/java/com/blog/service/ArticleService.java

1. 查询:用 @Cacheable 写入缓存

文章详情——按 id 缓存:

@Cacheable(value = "articles", key = "#id")
public Article getById(Long id) {
    return articleRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("文章不存在"));
}

文章分页列表——按页码、每页条数、关键词组合缓存:

@Cacheable(
        value = "articlePages",
        key = "#page + ':' + #size + ':' + (#keyword != null ? #keyword.trim() : '')")
public ArticlePageResult queryArticles(int page, int size, String keyword) {
    // ... 分页与搜索逻辑不变
}
注解属性含义
value缓存「命名空间」,对应 Redis 里的 cache name
key缓存键,支持 SpEL 表达式,#id 表示方法参数

Redis 中实际 key 形如:

articles::1
articles::42
articlePages::0:10:
articlePages::0:10:Spring
articlePages::1:10:Vue

格式为:{cacheName}::{key},由 Spring Cache 自动拼接。

2. 写操作:用 @CacheEvict 清除缓存

新建文章——列表缓存全部失效(新文章会出现在列表里):

@CacheEvict(value = "articlePages", allEntries = true)
@Transactional
public Article create(ArticleRequest request) {
    // ...
}

更新 / 删除——同时清掉该文章详情 + 所有列表页缓存:

@Caching(evict = {
        @CacheEvict(value = "articles", key = "#id"),
        @CacheEvict(value = "articlePages", allEntries = true)
})
@Transactional
public Article update(Long id, ArticleRequest request) {
    // ...
}

@Caching 用于在一个方法上组合多个缓存操作。

3. 缓存策略一览

方法注解行为
queryArticles@Cacheable读:先查 Redis,没有再查库并写入
getById@Cacheable读:同上
create@CacheEvict(articlePages)写:清空所有列表缓存
update@CacheEvict(articles + articlePages)写:清该 id 详情 + 所有列表
delete@CacheEvict(articles + articlePages)写:同上

为什么列表用 allEntries = true 而不是按 key 精确删除?

  • 列表 key 组合多(页码 × 每页条数 × 关键词),逐一维护成本高
  • 文章变更频率通常低于查询频率,整表清空列表缓存更简单可靠

七、一次请求的完整流程

场景 A:第一次访问 GET /api/articles?page=0&size=10

1. Controller 调用 articleService.queryArticles(0, 10, null)
2. AOP 根据 key "0:10:" 查 Redis → 未命中
3. 执行方法体:articleRepository.findAll(pageable)
4. 将 ArticlePageResult 写入 Redis(articlePages::0:10:)
5. 返回给前端

场景 B:10 秒内再次请求相同 URL

1. Controller 再次调用 queryArticles(0, 10, null)
2. AOP 查 Redis → 命中
3. 直接反序列化返回,不执行方法体,控制台不会出现 SQL

场景 C:用户修改文章后

1. PUT /api/articles/5
2. update() 执行前先清 articles::5 和全部 articlePages::*
3. 保存到 MySQL
4. 下次 GET 会重新查库并写入新缓存

八、三个核心注解对比

注解触发时机典型用途
@Cacheable方法成功返回后写入缓存;下次相同 key 直接返回缓存查询详情、列表
@CacheEvict方法执行时删除指定缓存增删改后保证数据一致
@Caching组合多个 @Cacheable / @CacheEvict一次写操作要清多处缓存

补充:@CachePut 每次都会执行方法并更新缓存(本项目未使用,更新场景用「先 evict 再查库」更直观)。


九、如何验证缓存已生效

1. 看 SQL 日志

application.ymlspring.jpa.show-sql: true 时:

  1. 第一次请求 GET /api/articles → 控制台打印 SELECT ...
  2. 立即再请求一次 → 不应再出现相同 SELECT

2. 用 redis-cli 看 key

redis-cli keys "*article*"

应能看到类似:

1) "articlePages::0:10:"
2) "articles::3"

查看某条缓存内容:

redis-cli get "articles::3"

3. 改文章后再看

执行 PUT /api/articles/3 后:

redis-cli keys "*article*"

articles::3articlePages::* 应被清除;再次 GET 会重新生成。


十、常见坑与注意事项

1. 同类内部调用不走缓存

update() 内部调用了 getById(id),这是 this.getById(),不经过 Spring 代理,不会触发 @Cacheable

对本项目这是合理行为:写操作前需要从数据库拿最新实体做权限校验,不应读旧缓存。

若要让内部调用也走缓存,需通过注入自身代理或拆到另一个 Service——一般不必。

2. Redis 未启动

启动报错类似 Unable to connect to Redis。先确认 redis-server 或容器在 6379 端口监听。

3. 缓存与事务

@CacheEvict 默认在方法执行后清缓存。写方法已加 @Transactional,数据库提交成功后再失效缓存,避免回滚后缓存却被清掉的极端情况(Spring 默认 evict 时机可配置,初学保持默认即可)。

4. 缓存的是 Java 对象,不是 SQL 结果

ArticlePageResultArticle 会被整体序列化进 Redis。实体字段变更(加字段、改类型)后,旧缓存 JSON 可能反序列化失败,开发期可 redis-cli flushall 清空。

5. 安全与部署

  • 生产环境为 Redis 设置密码,并在 spring.data.redis.password 中配置
  • 不要把 Redis 暴露到公网;仅应用内网访问
  • 敏感数据(如用户密码)不应放入缓存明文

十一、改造前 vs 改造后

维度改造前改造后
文章列表每次请求都查 MySQL相同 page/size/keyword 30 分钟内走 Redis
文章详情每次 findById相同 id 命中缓存
新建 / 改 / 删只写数据库写库 + 自动失效相关缓存
依赖MySQLMySQL + Redis
代码侵入Service 方法上几个注解,Controller 无变化

对前端完全透明:接口 URL、请求参数、响应 JSON 格式不变,只是响应变快。


十二、可扩展方向

需求思路
评论列表缓存CommentService.listByArticleId 上加 @Cacheable,key 用 articleId
按缓存名设置不同 TTLRedisCacheManager.withCacheConfiguration("articles", config)
热点文章浏览量RedisTemplate.opsForValue().increment("view:" + id)
接口限流Redis + 自定义注解或 Bucket4j
JWT 登出黑名单将 token 存入 Redis,过期时间与 JWT 一致
多实例部署多台 Spring Boot 共享同一 Redis,缓存天然一致

十三、本篇小结

Redis 在本项目中的定位,是 MySQL 前面的读缓存层,通过 Spring Cache 以声明式方式接入:

步骤文件做了什么
加依赖pom.xmldata-redis + cache
配连接application.ymlhost、port、TTL
配序列化RedisConfig.javaRedisCacheManager + RedisTemplate
加注解ArticleService.java读缓存、写失效

记住这条主线:

@Cacheable 查询 → Redis 命中则返回 / 未命中则查库并写入
@CacheEvict  写入 → 删 Redis 中过期数据 → 下次查询自动重建

与分页篇串联起来,完整读路径为:

@RequestParam@Cacheable → (Redis 或 Repository) → ArticlePageResult → ApiResponse

可在此基础上扩展 评论缓存Redis 计数JWT 黑名单登出