在现代应用开发中,缓存是提升系统性能的重要手段。Redis 作为高性能的内存数据库,在缓存领域应用广泛。Spring 框架提供了强大的缓存抽象,结合 Redis 可以实现简洁高效的缓存方案。本文将深入介绍如何在 Java 项目中使用注解方式开启 Redis 缓存。
一、Redis 缓存的优势
在开始之前,我们先了解为什么选择 Redis 作为缓存:
- 高性能:基于内存操作,读写速度极快
- 数据结构丰富:支持 String、Hash、List、Set、ZSet 等多种数据结构
- 持久化:支持 RDB 和 AOF 两种持久化方式,避免数据丢失
- 分布式支持:天然支持分布式环境,适合微服务架构
- 原子操作:提供丰富的原子操作命令
- 过期策略:支持键的过期时间设置
- 发布订阅:支持消息队列功能
二、Spring 缓存抽象
Spring 提供了一套缓存抽象,允许我们使用不同的缓存提供者(如 Redis、EhCache、Caffeine 等)而不需要修改业务逻辑。核心注解包括:
@EnableCaching
- 启用缓存功能@Cacheable
- 触发缓存读取@CachePut
- 触发缓存更新@CacheEvict
- 触发缓存删除@Caching
- 组合多个缓存操作@CacheConfig
- 类级别的缓存配置
三、环境准备
首先需要在项目中添加必要的依赖:
<!-- Maven依赖 -->
<dependencies>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 连接池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
然后在 application.properties 或 application.yml 中配置 Redis 连接信息:
# Redis配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
# 连接池配置
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
四、启用 Redis 缓存注解
在 Spring Boot 应用的主类上添加@EnableCaching
注解来启用缓存功能:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 启用缓存功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
五、配置 Redis 缓存管理器
为了让 Spring 使用 Redis 作为缓存提供者,我们需要配置 RedisCacheManager:
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认缓存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 设置缓存过期时间为10分钟
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}
这段配置代码做了以下几件事:
- 创建一个 RedisCacheManager Bean
- 配置默认的缓存策略,包括:
- 缓存过期时间为 10 分钟
- 使用 StringRedisSerializer 序列化键
- 使用 GenericJackson2JsonRedisSerializer 序列化值(以 JSON 格式存储)
- 禁用缓存 null 值
六、使用缓存注解
现在我们可以在服务层使用缓存注解了。以下是一些常见的用法示例:
1. @Cacheable - 缓存查询结果
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 使用@Cacheable注解,将方法返回值缓存到"users"缓存中
// key使用SpEL表达式,基于方法参数生成
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("从数据库查询用户ID: " + id);
// 模拟从数据库查询
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
// 条件缓存:只有当用户年龄大于18时才缓存
@Cacheable(value = "users", key = "#id", condition = "#result.age > 18")
public User getUserByIdWithCondition(Long id) {
System.out.println("从数据库查询用户ID: " + id);
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
// 除非条件:结果为null时不缓存
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserByIdUnlessNull(Long id) {
System.out.println("从数据库查询用户ID: " + id);
return userRepository.findById(id).orElse(null);
}
}
2. @CachePut - 更新缓存
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 使用@CachePut更新缓存,无论缓存是否存在都会执行方法
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
System.out.println("更新用户信息: " + user.getId());
// 保存到数据库并返回更新后的对象
return userRepository.save(user);
}
}
3. @CacheEvict - 删除缓存
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 使用@CacheEvict删除缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
System.out.println("删除用户: " + id);
userRepository.deleteById(id);
}
// 删除"users"缓存中的所有条目
@CacheEvict(value = "users", allEntries = true)
public void deleteAllUsers() {
System.out.println("删除所有用户");
userRepository.deleteAll();
}
// 删除后执行(删除数据库记录后再删除缓存)
@CacheEvict(value = "users", key = "#id", beforeInvocation = false)
public void deleteUserAfterInvocation(Long id) {
System.out.println("删除用户: " + id);
userRepository.deleteById(id);
}
}
4. @Caching - 组合多个缓存操作
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 使用@Caching组合多个缓存操作
@Caching(
put = {
@CachePut(value = "users", key = "#user.id"),
@CachePut(value = "usersByName", key = "#user.name")
},
evict = {
@CacheEvict(value = "recentUsers", allEntries = true)
}
)
public User complexUpdate(User user) {
System.out.println("执行复杂更新: " + user.getId());
return userRepository.save(user);
}
}
5. @CacheConfig - 类级别的缓存配置
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@CacheConfig(cacheNames = "users") // 类级别缓存配置
public class UserService {
// 无需指定value,继承类级别的cacheNames
@Cacheable(key = "#id")
public User getUserById(Long id) {
System.out.println("从数据库查询用户ID: " + id);
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
// 可以覆盖类级别的配置
@Cacheable(value = "specialUsers", key = "#id")
public User getSpecialUserById(Long id) {
System.out.println("从数据库查询特殊用户ID: " + id);
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
七、缓存键生成策略
Spring 默认使用 SimpleKeyGenerator 生成缓存键,但我们也可以自定义键生成策略:
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("myKeyGenerator")
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 自定义键生成策略,例如:类名+方法名+参数
return target.getClass().getSimpleName() + "_" +
method.getName() + "_" +
Arrays.deepHashCode(params);
}
}
然后在注解中使用:
@Cacheable(value = "users", keyGenerator = "myKeyGenerator")
public User getUserById(Long id) {
// ...
}
八、缓存配置进阶
1. 自定义缓存管理器
我们可以创建多个缓存管理器,用于不同的缓存需求:
@Bean
public RedisCacheManager customCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config1 = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
RedisCacheConfiguration config2 = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.withCacheConfiguration("shortTermCache", config1)
.withCacheConfiguration("longTermCache", config2)
.build();
}
2. 缓存穿透、缓存击穿和缓存雪崩解决方案
缓存穿透
// 使用@Cacheable的unless属性避免缓存null值
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 从数据库查询
User user = userRepository.findById(id).orElse(null);
// 对于不存在的用户,可以缓存一个特殊对象
if (user == null) {
// 可以记录到一个特殊的缓存中
return null;
}
return user;
}
缓存击穿
// 使用sync属性,确保只有一个线程去加载数据
@Cacheable(value = "hotProducts", key = "#id", sync = true)
public Product getHotProduct(Long id) {
// 从数据库加载热点数据
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
缓存雪崩
通过设置不同的过期时间,避免大量缓存同时失效:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 使用随机过期时间,避免缓存雪崩
Random random = new Random();
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10 + random.nextInt(5))) // 10-15分钟随机过期
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
九、监控和调试
Spring Boot Actuator 提供了缓存监控端点:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启用缓存端点:
management.endpoints.web.exposure.include=cache*
访问以下端点查看缓存信息:
/actuator/caches
- 获取所有缓存名称/actuator/caches/{cacheName}
- 获取指定缓存的信息/actuator/caches/{cacheName}/{key}
- 获取指定缓存中特定键的值
十、最佳实践
- 合理设置缓存过期时间:根据业务需求设置合适的过期时间,避免缓存数据长时间不更新
- 选择合适的缓存键:确保缓存键的唯一性和可读性
- 注意缓存一致性:在更新数据时及时更新缓存
- 避免缓存穿透:对不存在的数据也进行缓存
- 防止缓存雪崩:设置不同的过期时间,避免大量缓存同时失效
- 监控缓存使用情况:定期检查缓存命中率,优化缓存策略
- 考虑分布式环境:在分布式系统中,确保缓存更新操作的原子性
- 测试缓存逻辑:编写单元测试验证缓存行为
十一、总结
通过 @EnableCaching 注解和 Spring 的缓存抽象,我们可以非常方便地在 Java 应用中集成 Redis 缓存。这种声明式的缓存方式大大简化了代码,使我们能够专注于业务逻辑而不是缓存实现细节。
在实际应用中,我们需要根据业务特点合理配置缓存策略,注意缓存一致性问题,并采取措施防止缓存穿透、击穿和雪崩等常见问题。