【Java全栈教程】第 44 课:Spring Boot缓存

1 阅读5分钟

第 44 课:Spring Boot缓存

本课目标:掌握Spring Cache抽象层,集成Redis实现缓存,理解缓存穿透/击穿/雪崩问题及解决方案。


一、概念讲解

1.1 为什么需要缓存

无缓存流程:
数据库 ──> 100ms ──> 返回结果
每次请求都访问数据库,压力大

有缓存流程:
缓存(1ms) ──> 命中 ──> 返回结果
缓存(1ms) ──> 未命中 ──> 数据库(100ms) ──> 写入缓存 ──> 返回结果

1.2 Spring Cache注解体系

┌─────────────────────────────────────────────────┐
│                 Spring Cache 核心注解             │
├─────────────────────────────────────────────────┤
│  @Cacheable    查询:先查缓存,有则返回,无则查库  │
│  @CachePut     更新:每次执行方法,更新缓存        │
│  @CacheEvict   删除:清除指定缓存                 │
│  @Caching      组合:同时操作多个缓存             │
│  @CacheConfig  类级别:统一配置缓存名称等         │
└─────────────────────────────────────────────────┘

1.3 缓存常见问题

缓存穿透(查不存在的数据):
请求 ──> 缓存(无) ──> 数据库(无) ──> 返回null
→ 恶意请求大量查询不存在的ID,绕过缓存直接打数据库
→ 解决:缓存空值 + 布隆过滤器

缓存击穿(热key过期):
缓存过期 ──> 大量请求同时查库 ──> 数据库压力骤增
→ 解决:互斥锁(只放一个请求查库)+ 永不过期

缓存雪崩(大量key同时过期):
大量缓存同时过期 ──> 所有请求都打到数据库
→ 解决:随机过期时间 + 集群部署 + 熔断降级

1.4 Redis数据结构

String:   key → value (最常用)
Hash:     key → field → value (对象存储)
List:     key → [value1, value2, ...] (队列)
Set:      key → {value1, value2, ...} (集合)
ZSet:     key → score → value (排行榜)

二、语法格式

2.1 @Cacheable用法

// 基本用法
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { ... }

// 条件缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User findById(Long id) { ... }

// 不缓存null值
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) { ... }

2.2 @CacheEvict用法

// 删除单个缓存
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) { ... }

// 清空整个缓存
@CacheEvict(value = "users", allEntries = true)
public void clearCache() { ... }

三、代码案例

3.1 添加依赖

<!-- Spring Cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 Redis配置

# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 3000ms

3.3 Redis配置类

package com.example.demo.config;

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 org.springframework.cache.CacheManager;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // JSON序列化
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL);

        GenericJackson2JsonRedisSerializer jsonSerializer =
            new GenericJackson2JsonRedisSerializer(om);
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();

        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // JSON序列化配置
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL);

        GenericJackson2JsonRedisSerializer jsonSerializer =
            new GenericJackson2JsonRedisSerializer(om);

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))          // 默认过期30分钟
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(jsonSerializer))
            .disableCachingNullValues();               // 不缓存null

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

3.4 带缓存的Service

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@CacheConfig(cacheNames = "users")
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 查询:先查缓存,有则返回,无则查库并缓存
    @Cacheable(key = "#id", unless = "#result == null")
    public User findById(Long id) {
        simulateSlowService();
        return userRepository.findById(id).orElse(null);
    }

    // 查询所有:缓存整个列表
    @Cacheable(key = "'all'")
    public List<User> findAll() {
        simulateSlowService();
        return userRepository.findAll();
    }

    // 更新:每次执行方法,更新缓存
    @CachePut(key = "#user.id")
    public User update(User user) {
        User existing = userRepository.findById(user.getId())
            .orElseThrow(() -> new RuntimeException("用户不存在"));
        existing.setUsername(user.getUsername());
        existing.setEmail(user.getEmail());
        existing.setAge(user.getAge());
        return userRepository.save(existing);
    }

    // 删除:清除指定缓存
    @CacheEvict(key = "#id")
    public void deleteById(Long id) {
        userRepository.deleteById(id);
    }

    // 清空所有缓存
    @CacheEvict(allEntries = true)
    public void clearAllCache() {
        System.out.println("缓存已清空");
    }

    // 组合操作
    @Caching(
        put = { @CachePut(key = "#user.id") },
        evict = { @CacheEvict(key = "'all'") }
    )
    public User save(User user) {
        return userRepository.save(user);
    }

    // 模拟慢查询(测试用)
    private void simulateSlowService() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

3.5 缓存Controller

package com.example.demo.controller;

import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.findById(id);
        long cost = System.currentTimeMillis() - start;
        System.out.println("查询耗时: " + cost + "ms");
        return Result.success(user);
    }

    @GetMapping
    public Result<List<User>> list() {
        long start = System.currentTimeMillis();
        List<User> users = userService.findAll();
        long cost = System.currentTimeMillis() - start;
        System.out.println("查询耗时: " + cost + "ms");
        return Result.success(users);
    }

    @PutMapping("/{id}")
    public Result<User> update(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        return Result.success(userService.update(user));
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.deleteById(id);
        return Result.success("删除成功", null);
    }

    @PostMapping("/clear-cache")
    public Result<Void> clearCache() {
        userService.clearAllCache();
        return Result.success("缓存已清空", null);
    }
}

3.6 手动操作Redis(RedisTemplate)

package com.example.demo.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // 设置值
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    // 设置值(带过期时间)
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    // 获取值
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // 删除key
    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }

    // 判断key是否存在
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    // 设置过期时间
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    // 自增
    public Long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    // Hash操作
    public void hSet(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    public Object hGet(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }
}

3.7 缓存预热(启动时加载)

package com.example.demo.config;

import com.example.demo.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmUp implements CommandLineRunner {

    private final UserService userService;

    public CacheWarmUp(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void run(String... args) {
        System.out.println("=== 缓存预热开始 ===");
        userService.findAll(); // 预热用户列表缓存
        System.out.println("=== 缓存预热完成 ===");
    }
}

▶ 运行结果:

=== 缓存预热开始 ===
=== 缓存预热完成 ===

四、常见错误

4.1 缓存不生效

原因

  1. 未在启动类添加@EnableCaching
  2. 调用方是类内部方法(自调用不走代理)
  3. key表达式写错

解决:确保注解正确,避免自调用。

4.2 序列化异常

现象:从Redis取出的对象类型不对。

解决:配置正确的序列化器(如GenericJackson2JsonRedisSerializer),或在实体类中添加类型信息。

4.3 Redis连接失败

现象Unable to connect to Redis

解决:检查Redis服务是否启动,spring.redis.hostport配置是否正确。

4.4 缓存与数据库不一致

原因:更新数据库后未及时更新/删除缓存。

解决:使用@CacheEvict在数据变更时清除缓存。


五、课后练习

  1. 为商品(Product)模块添加缓存,测试有缓存和无缓存的性能差异
  2. 实现一个缓存监控接口,返回Redis中缓存的key数量和内存占用
  3. 模拟缓存穿透:查询不存在的ID,观察数据库压力,然后添加空值缓存解决
  4. 使用@CacheConfig统一配置缓存过期时间为1小时

六、本课小结

知识点说明
@Cacheable查询缓存,先查缓存再查库
@CachePut更新缓存,每次执行方法后更新
@CacheEvict清除缓存,数据变更时使用
Redis内存数据库,高性能缓存方案
缓存穿透查询不存在的数据,解决:缓存空值/布隆过滤器
缓存击穿热key过期,解决:互斥锁/永不过期
缓存雪崩大量key同时过期,解决:随机过期时间

本课程持续更新中,欢迎关注!