Java 中使用 Redis 注解版缓存——补充
在现代应用开发中,缓存是提升系统性能的重要手段。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 {@Beanpublic 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 {@Overridepublic 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 缓存。这种声明式的缓存方式大大简化了代码,使我们能够专注于业务逻辑而不是缓存实现细节。
在实际应用中,我们需要根据业务特点合理配置缓存策略,注意缓存一致性问题,并采取措施防止缓存穿透、击穿和雪崩等常见问题。