9. Spring Boot 缓存优化

3 阅读7分钟

一、缓存概述

1. 什么是缓存?

缓存:将数据存储在内存中,以减少数据库查询次数,提高系统性能。


2. 为什么需要缓存?

场景问题缓存解决方案
频繁查询相同数据每次都查询数据库,响应慢缓存查询结果
数据不常变化数据库压力大缓存减少数据库压力
高并发访问数据库成为瓶颈缓存分担负载
热点数据大量请求访问同一数据缓存提供快速响应

3. 缓存类型对比

缓存类型优点缺点适用场景
本地缓存(Caffeine)速度快、无需网络容量有限、无法共享单机应用
分布式缓存(Redis)容量大、可共享需要网络、延迟略高分布式应用

二、Spring Cache 简介

1. 核心注解

注解作用示例
@Cacheable缓存方法返回值@Cacheable("users")
@CachePut更新缓存@CachePut(value = "users", key = "#id")
@CacheEvict清除缓存@CacheEvict(value = "users", key = "#id")
@Caching组合多个缓存操作@Caching(cacheable = {...}, evict = {...})
@CacheConfig类级别缓存配置@CacheConfig(cacheNames = "users")

2. 添加依赖

<!-- 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>

<!-- Caffeine(本地缓存) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

3. 启用缓存

启动类添加注解:

@SpringBootApplication
@EnableCaching  // 启用缓存
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

三、Redis 缓存配置

1. Redis 配置

application.properties:

# Redis 配置
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.database=0

# Redis 连接池配置
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1ms

# 缓存配置
spring.cache.type=redis
spring.cache.redis.time-to-live=600000  # 10 分钟
spring.cache.redis.cache-null-values=false  # 不缓存空值
spring.cache.redis.key-prefix="myapp:"  # Key 前缀
spring.cache.redis.use-key-prefix=true

2. Redis 配置类

RedisConfig.java:

package com.example.myapp.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.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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {
    
    // RedisTemplate 配置
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory
    ) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // JSON 序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = 
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL
        );
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        
        // Key 使用 String 序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        
        // Key 采用 String 序列化
        template.setKeySerializer(stringRedisSerializer);
        // Hash Key 采用 String 序列化
        template.setHashKeySerializer(stringRedisSerializer);
        // Value 采用 Jackson 序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash Value 采用 Jackson 序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    // RedisCacheManager 配置
    @Bean
    public RedisCacheManager cacheManager(
            RedisConnectionFactory connectionFactory
    ) {
        // JSON 序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = 
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL
        );
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        
        // 配置缓存
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))  // 默认过期时间 10 分钟
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();  // 不缓存空值
        
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

四、缓存注解使用

1. @Cacheable(缓存读取)

场景:查询用户时缓存结果

@Service
public class UserService {
    
    // 缓存查询结果
    @Cacheable(
            value = "users",           // 缓存名称
            key = "#id",               // 缓存 Key(使用方法参数 id)
            unless = "#result == null" // 结果为 null 时不缓存
    )
    public UserDTO findById(Long id) {
        // 第一次执行:查询数据库
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        return UserDTO.fromEntity(user);
        // 结果被缓存到 Redis
    }
}

工作原理:

1. 调用 findById(1)
   ↓
2. 检查缓存中是否存在 key="users::1"
   ↓
3. 存在:直接返回缓存数据(不执行方法)
   不存在:执行方法,查询数据库
   ↓
4. 将结果写入缓存
   ↓
5. 第二次调用 findById(1):直接从缓存返回

2. @CachePut(更新缓存)

场景:更新用户时更新缓存

@Service
public class UserService {
    
    // 更新缓存
    @CachePut(
            value = "users",
            key = "#id"
    )
    public UserDTO update(Long id, UpdateUserDTO userDTO) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        
        // 更新用户
        if (userDTO.getName() != null) {
            user.setName(userDTO.getName());
        }
        
        User updatedUser = userRepository.save(user);
        
        // 结果更新到缓存
        return UserDTO.fromEntity(updatedUser);
    }
}

工作原理:

1. 调用 update(1, userDTO)
   ↓
2. 执行方法,更新数据库
   ↓
3. 将新结果更新到缓存 key="users::1"
   ↓
4. 下次查询:获取最新的缓存数据

3. @CacheEvict(清除缓存)

场景:删除用户时清除缓存

@Service
public class UserService {
    
    // 清除缓存
    @CacheEvict(
            value = "users",
            key = "#id"
    )
    public void delete(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        userRepository.delete(user);
        // 缓存 key="users::id" 被清除
    }
    
    // 清除所有缓存
    @CacheEvict(
            value = "users",
            allEntries = true
    )
    public void clearAllCache() {
        // 清除 users 缓存下的所有数据
    }
}

4. @Caching(组合操作)

场景:同时更新多个缓存

@Service
public class UserService {
    
    @Caching(
        cacheable = {
            @Cacheable(value = "users", key = "#id")
        },
        evict = {
            @CacheEvict(value = "userList", allEntries = true)
        }
    )
    public UserDTO findById(Long id) {
        // 缓存到 users,同时清除 userList 缓存
    }
}

5. @CacheConfig(类级别配置)

场景:统一配置缓存的 cacheNames

@Service
@CacheConfig(cacheNames = "users")
public class UserService {
    
    // 不需要指定 value
    @Cacheable(key = "#id")
    public UserDTO findById(Long id) {
        // ...
    }
    
    @CachePut(key = "#id")
    public UserDTO update(Long id, UpdateUserDTO userDTO) {
        // ...
    }
    
    @CacheEvict(key = "#id")
    public void delete(Long id) {
        // ...
    }
}

五、自定义缓存 Key

1. Key 表达式

表达式说明示例
#id使用方法参数 id 的值@Cacheable(key = "#id")
#user.id使用 user 对象的 id 属性@Cacheable(key = "#user.id")
#root.methodName使用方法名@Cacheable(key = "#root.methodName")
#root.args[0]使用第一个参数@Cacheable(key = "#root.args[0]")
#p0使用第一个参数(简写)@Cacheable(key = "#p0")
T(Math).random()调用静态方法@Cacheable(key = "user_" + #id + "_" + T(Math).random())

2. 自定义 Key 生成器

CustomKeyGenerator.java:

package com.example.myapp.config;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.StringJoiner;

@Component
public class CustomKeyGenerator implements KeyGenerator {
    
    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder builder = new StringBuilder();
        builder.append(target.getClass().getSimpleName());
        builder.append(".");
        builder.append(method.getName());
        builder.append("(");
        
        StringJoiner joiner = new StringJoiner(",");
        for (Object param : params) {
            joiner.add(param != null ? param.toString() : "null");
        }
        builder.append(joiner);
        builder.append(")");
        
        return builder.toString();
    }
}

使用自定义 Key 生成器:

@Cacheable(
    value = "users",
    keyGenerator = "customKeyGenerator"
)
public UserDTO findById(Long id) {
    // Key: UserService.findById(1)
}

六、Caffeine 本地缓存

1. Caffeine 配置

application.properties:

# Caffeine 配置
spring.cache.type=caffeine
spring.cache.cache-names=users,products
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=10m

2. Caffeine 配置类

CaffeineConfig.java:

package com.example.myapp.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CaffeineConfig {
    
    @Bean
    public Caffeine<Object, Object> caffeineConfig() {
        return Caffeine.newBuilder()
                .initialCapacity(100)    // 初始容量
                .maximumSize(1000)      // 最大容量
                .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后 10 分钟过期
                .recordStats();        // 记录缓存统计信息
    }
    
    @Bean
    public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(caffeine);
        return caffeineCacheManager;
    }
}

3. Caffeine 使用示例

@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#id")
    public UserDTO findById(Long id) {
        // 使用 Caffeine 本地缓存
    }
}

七、Redis 缓存 vs Caffeine 缓存

维度Redis 缓存Caffeine 缓存
类型分布式缓存本地缓存
速度略慢(网络延迟)极快(内存)
容量大(取决于服务器)小(取决于 JVM 堆内存)
共享多实例共享单机独享
持久化支持久化不支持
适用场景分布式系统单机应用、热点数据

八、缓存穿透、雪崩、击穿

1. 缓存穿透

定义:查询不存在的数据,每次都绕过缓存直接查询数据库。

解决方案

  • 缓存空值(unless = "#result == null"
  • 布隆过滤器
@Cacheable(
    value = "users",
    key = "#id",
    unless = "#result == null"  // 结果为 null 时不缓存
)
public UserDTO findById(Long id) {
    // ...
}

2. 缓存雪崩

定义:大量缓存同时失效,所有请求都打到数据库。

解决方案

  • 设置不同的过期时间
  • 使用互斥锁(Redis SETNX)
@Cacheable(
    value = "users",
    key = "#id",
    unless = "#result == null"
)
public UserDTO findById(Long id) {
    // 随机过期时间
    long randomExpire = 10 + (long) (Math.random() * 5);
    // ...
}

3. 缓存击穿

定义:热点数据过期,大量请求同时查询该数据。

解决方案

  • 互斥锁(Redis SETNX)
  • 永不过期(后台更新)
@Cacheable(
    value = "users",
    key = "#id",
    sync = true  // 同步缓存更新
)
public UserDTO findById(Long id) {
    // ...
}

九、缓存实战示例

1. 完整的 UserService

package com.example.myapp.service;

import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.model.User;
import com.example.myapp.repository.UserRepository;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional
@CacheConfig(cacheNames = "users")
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // 缓存查询
    @Cacheable(key = "#id", unless = "#result == null")
    public UserDTO findById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        return UserDTO.fromEntity(user);
    }
    
    // 创建用户(清除缓存)
    @CacheEvict(allEntries = true)
    public UserDTO create(CreateUserDTO userDTO) {
        if (userRepository.existsByEmail(userDTO.getEmail())) {
            throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
        }
        
        User user = new User();
        user.setName(userDTO.getName());
        user.setEmail(userDTO.getEmail());
        user.setPassword(userDTO.getPassword());
        user.setPhone(userDTO.getPhone());
        
        User savedUser = userRepository.save(user);
        return UserDTO.fromEntity(savedUser);
    }
    
    // 更新用户(更新缓存)
    @CachePut(key = "#id")
    public UserDTO update(Long id, UpdateUserDTO userDTO) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        
        if (userDTO.getEmail() != null && 
            !userDTO.getEmail().equals(user.getEmail()) && 
            userRepository.existsByEmail(userDTO.getEmail())) {
            throw new IllegalArgumentException("邮箱已被使用: " + userDTO.getEmail());
        }
        
        if (userDTO.getName() != null) {
            user.setName(userDTO.getName());
        }
        if (userDTO.getEmail() != null) {
            user.setEmail(userDTO.getEmail());
        }
        if (userDTO.getPhone() != null) {
            user.setPhone(userDTO.getPhone());
        }
        if (userDTO.getActive() != null) {
            user.setActive(userDTO.getActive());
        }
        
        User updatedUser = userRepository.save(user);
        return UserDTO.fromEntity(updatedUser);
    }
    
    // 删除用户(清除缓存)
    @CacheEvict(key = "#id")
    public void delete(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", id));
        userRepository.delete(user);
    }
    
    // 搜索用户(不缓存)
    public List<UserDTO> searchByName(String name) {
        return userRepository.findByNameContainingIgnoreCase(name).stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }
    
    // 清除所有缓存
    @CacheEvict(allEntries = true)
    public void clearAllCache() {
        // 清除所有缓存
    }
}

2. 测试缓存效果

测试代码:

@SpringBootTest
public class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Test
    public void testCache() {
        // 第一次查询:查询数据库
        UserDTO user1 = userService.findById(1L);
        
        // 检查 Redis 中是否存在缓存
        String cacheKey = "users::1";
        Object cachedUser = redisTemplate.opsForValue().get(cacheKey);
        System.out.println("缓存中的数据: " + cachedUser);
        
        // 第二次查询:从缓存读取
        UserDTO user2 = userService.findById(1L);
        
        // 清除缓存
        userService.clearAllCache();
    }
}

十、总结

概念说明
Spring CacheSpring 缓存抽象层
Redis 缓存分布式缓存
Caffeine 缓存本地缓存
@Cacheable缓存读取
@CachePut更新缓存
@CacheEvict清除缓存
缓存穿透查询不存在的数据
缓存雪崩大量缓存同时失效
缓存击穿热点数据过期