Java 中使用 Redis 注解版缓存——补充

发布于:2025-06-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

在现代应用开发中,缓存是提升系统性能的重要手段。Redis 作为高性能的内存数据库,在缓存领域应用广泛。Spring 框架提供了强大的缓存抽象,结合 Redis 可以实现简洁高效的缓存方案。本文将深入介绍如何在 Java 项目中使用注解方式开启 Redis 缓存。

一、Redis 缓存的优势

在开始之前,我们先了解为什么选择 Redis 作为缓存:

  1. 高性能:基于内存操作,读写速度极快
  2. 数据结构丰富:支持 String、Hash、List、Set、ZSet 等多种数据结构
  3. 持久化:支持 RDB 和 AOF 两种持久化方式,避免数据丢失
  4. 分布式支持:天然支持分布式环境,适合微服务架构
  5. 原子操作:提供丰富的原子操作命令
  6. 过期策略:支持键的过期时间设置
  7. 发布订阅:支持消息队列功能

二、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();
    }
}

这段配置代码做了以下几件事:

  1. 创建一个 RedisCacheManager Bean
  2. 配置默认的缓存策略,包括:
  • 缓存过期时间为 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} - 获取指定缓存中特定键的值

十、最佳实践

  1. 合理设置缓存过期时间:根据业务需求设置合适的过期时间,避免缓存数据长时间不更新
  2. 选择合适的缓存键:确保缓存键的唯一性和可读性
  3. 注意缓存一致性:在更新数据时及时更新缓存
  4. 避免缓存穿透:对不存在的数据也进行缓存
  5. 防止缓存雪崩:设置不同的过期时间,避免大量缓存同时失效
  6. 监控缓存使用情况:定期检查缓存命中率,优化缓存策略
  7. 考虑分布式环境:在分布式系统中,确保缓存更新操作的原子性
  8. 测试缓存逻辑:编写单元测试验证缓存行为

十一、总结

通过 @EnableCaching 注解和 Spring 的缓存抽象,我们可以非常方便地在 Java 应用中集成 Redis 缓存。这种声明式的缓存方式大大简化了代码,使我们能够专注于业务逻辑而不是缓存实现细节。

在实际应用中,我们需要根据业务特点合理配置缓存策略,注意缓存一致性问题,并采取措施防止缓存穿透、击穿和雪崩等常见问题。