Spring Framework缓存抽象:CacheManager与@Cacheable

Spring Framework缓存抽象:CacheManager与@Cacheable

【免费下载链接】spring-framework 【免费下载链接】spring-framework 项目地址: https://gitcode.com/gh_mirrors/spr/spring-framework

在现代应用开发中,频繁的数据查询操作往往会成为系统性能的瓶颈。每次请求都直接访问数据库不仅增加了数据库负担,还延长了响应时间。Spring Framework提供的缓存抽象(Cache Abstraction)通过在内存中存储常用数据,显著减少了对数据库的访问次数,从而提升系统性能。本文将详细介绍Spring缓存抽象的核心组件CacheManager与@Cacheable注解的使用方法,帮助开发者轻松实现高效缓存策略。

缓存抽象核心组件

Spring缓存抽象的核心设计理念是将缓存操作与业务逻辑解耦,通过统一的接口和注解简化缓存管理。其主要组件包括Cache接口、CacheManager接口和一系列缓存注解。

Cache接口

Cache接口定义了缓存操作的基本方法,如获取缓存项、放入缓存项、删除缓存项等。Spring提供了多种Cache接口的实现,以适应不同的缓存需求。常见的实现包括基于内存的ConcurrentMapCache、基于Redis的RedisCache、基于Caffeine的CaffeineCache等。开发者可以根据项目的实际需求选择合适的缓存实现。

CacheManager接口

CacheManager是Spring缓存抽象的核心接口,负责管理Cache实例。它的主要作用是根据缓存名称获取对应的Cache对象,并维护缓存的生命周期。

CacheManager接口类图

CacheManager接口的源码位于spring-context/src/main/java/org/springframework/cache/CacheManager.java,其定义如下:

public interface CacheManager {
    @Nullable
    Cache getCache(String name);
    Collection<String> getCacheNames();
}
  • getCache(String name):根据缓存名称获取对应的Cache实例。如果指定名称的缓存不存在,可能返回null,具体取决于实现类的策略。
  • getCacheNames():返回当前CacheManager管理的所有缓存名称的集合。

Spring提供了多种CacheManager的实现,以支持不同的缓存技术:

CacheManager实现类说明
ConcurrentMapCacheManager使用ConcurrentHashMap作为缓存存储,适用于简单的内存缓存场景
RedisCacheManager集成Redis作为缓存存储,适用于分布式缓存场景
CaffeineCacheManager集成Caffeine缓存库,提供高性能的内存缓存
EhCacheCacheManager集成EhCache缓存库,支持复杂的缓存配置

缓存注解

Spring提供了一系列注解来简化缓存操作,常用的包括:

  • @Cacheable:标记方法的返回结果可以被缓存。当方法被调用时,首先检查缓存中是否存在对应的数据,如果存在则直接返回缓存数据,否则执行方法并将结果存入缓存。
  • @CacheEvict:标记方法执行后需要清除缓存中的某些数据。
  • @CachePut:标记方法执行后将结果存入缓存,无论缓存中是否已存在对应数据。
  • @Caching:组合多个缓存注解,用于复杂的缓存操作场景。
  • @CacheConfig:在类级别统一配置缓存相关属性,如缓存名称、KeyGenerator等。

CacheManager详解

CacheManager作为缓存管理的核心,负责创建、配置和管理Cache实例。不同的CacheManager实现适用于不同的场景,下面详细介绍几种常用的CacheManager。

ConcurrentMapCacheManager

ConcurrentMapCacheManager是Spring提供的默认CacheManager实现,它使用ConcurrentHashMap作为底层存储,适用于单应用、简单的内存缓存场景。

使用ConcurrentMapCacheManager非常简单,只需在Spring配置类中进行如下配置:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        // 设置默认缓存名称
        cacheManager.setCacheNames(Arrays.asList("users", "products"));
        return cacheManager;
    }
}

上述配置创建了一个ConcurrentMapCacheManager实例,并指定了默认的缓存名称为"users"和"products"。当应用启动时,CacheManager会自动创建这些缓存。

RedisCacheManager

在分布式系统中,使用本地内存缓存(如ConcurrentMapCache)无法实现缓存共享,此时RedisCacheManager是更好的选择。RedisCacheManager集成了Redis数据库,将缓存数据存储在Redis中,实现了分布式环境下的缓存共享。

要使用RedisCacheManager,首先需要添加Redis相关依赖。在Maven项目中,可以在pom.xml中添加以下依赖:

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

然后在配置类中配置RedisCacheManager:

@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 设置默认缓存过期时间为10分钟
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        // 为不同的缓存设置不同的过期时间
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("users", config.entryTtl(Duration.ofHours(1)));
        configMap.put("products", config.entryTtl(Duration.ofMinutes(5)));
        
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .withInitialCacheConfigurations(configMap)
                .build();
    }
}

上述配置中,我们通过RedisCacheConfiguration设置了缓存的默认过期时间、键值序列化方式等。同时,为"users"和"products"缓存设置了不同的过期时间,以满足不同业务数据的缓存需求。

@Cacheable注解深度解析

@Cacheable注解是Spring缓存抽象中最常用的注解之一,它可以轻松地将方法的返回结果缓存起来。下面详细介绍@Cacheable注解的属性和使用方法。

@Cacheable注解属性

@Cacheable注解的源码位于spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java,其主要属性如下:

属性名说明
value/cacheNames指定缓存名称,可以指定多个,数组形式
key缓存键的SpEL表达式,用于动态生成缓存键
keyGenerator自定义缓存键生成器的bean名称
cacheManager指定使用的CacheManager的bean名称
cacheResolver指定使用的CacheResolver的bean名称
condition缓存条件的SpEL表达式,为true时才缓存
unless否决缓存的SpEL表达式,为true时不缓存
sync是否同步缓存,默认为false

基本使用

使用@Cacheable注解非常简单,只需在需要缓存结果的方法上添加该注解,并指定缓存名称:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        System.out.println("查询用户信息,id: " + id);
        return userRepository.findById(id).orElse(null);
    }
}

上述代码中,@Cacheable(value = "users", key = "#id")表示将getUserById方法的返回结果缓存到名为"users"的缓存中,缓存键为方法参数id的值。当第一次调用该方法时,会执行方法体并将结果存入缓存;后续调用时,如果缓存中存在对应id的用户信息,则直接返回缓存数据,不再执行方法体。

缓存键生成

缓存键的生成是缓存操作的关键,@Cacheable注解提供了多种方式来生成缓存键。

默认键生成器

如果不指定keykeyGenerator属性,Spring会使用默认的键生成器SimpleKeyGeneratorSimpleKeyGenerator的生成规则如下:

  • 如果方法没有参数,则使用SimpleKey.EMPTY作为键。
  • 如果方法有一个参数,则使用该参数的值作为键。
  • 如果方法有多个参数,则使用包含所有参数值的SimpleKey对象作为键。
自定义key属性

通过key属性可以使用SpEL表达式动态生成缓存键。例如:

@Cacheable(value = "users", key = "#user.id")
public User getUserByUser(User user) {
    return userRepository.findById(user.getId()).orElse(null);
}

@Cacheable(value = "products", key = "'product_' + #name")
public Product getProductByName(String name) {
    return productRepository.findByName(name).orElse(null);
}

上述代码中,key = "#user.id"表示使用参数user的id属性作为缓存键;key = "'product_' + #name"表示使用字符串"product_"拼接参数name作为缓存键。

自定义KeyGenerator

如果默认的键生成规则和key属性的SpEL表达式都无法满足需求,可以自定义KeyGenerator。

首先,创建一个实现KeyGenerator接口的类:

@Component
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        // 生成缓存键,例如:类名+方法名+参数值
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getName()).append(":");
        sb.append(method.getName()).append(":");
        for (Object param : params) {
            sb.append(param).append(",");
        }
        return sb.toString();
    }
}

然后,在@Cacheable注解中指定keyGenerator属性:

@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUserByUsername(String username) {
    return userRepository.findByUsername(username).orElse(null);
}

缓存条件

@Cacheable注解的conditionunless属性可以用于控制缓存的条件。

condition属性

condition属性用于指定缓存的条件,当SpEL表达式的值为true时才进行缓存。例如:

@Cacheable(value = "users", condition = "#id > 10")
public User getUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

上述代码中,只有当id大于10时,才会缓存方法的返回结果。

unless属性

unless属性用于指定否决缓存的条件,当SpEL表达式的值为true时不进行缓存。unless表达式可以访问方法的返回结果(使用#result变量)。例如:

@Cacheable(value = "users", unless = "#result == null")
public User getUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

上述代码中,如果方法返回的User对象为null,则不进行缓存。

同步缓存

sync属性用于设置是否同步缓存,默认为false。当sync = true时,多个线程同时请求同一个缓存键的数据时,只有一个线程会执行方法体,其他线程会等待该线程执行完成并将结果存入缓存后,直接获取缓存数据。这可以有效防止缓存击穿问题。

@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

实际应用案例

下面通过一个完整的案例来演示Spring缓存抽象的实际应用。

项目结构

本案例的项目结构如下:

gh_mirrors/spr/spring-framework/
├── spring-context/
│   └── src/
│       └── main/
│           └── java/
│               └── org/
│                   └── springframework/
│                       └── cache/
│                           ├── Cache.java
│                           ├── CacheManager.java
│                           └── annotation/
│                               └── Cacheable.java
├── ...
└── src/
    └── main/
        └── java/
            └── com/
                └── example/
                    ├── config/
                    │   └── CacheConfig.java
                    ├── model/
                    │   └── User.java
                    ├── repository/
                    │   └── UserRepository.java
                    └── service/
                        └── UserService.java

配置CacheManager

在配置类中配置CacheManager:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        // 使用ConcurrentMapCacheManager作为缓存管理器
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        // 设置缓存名称
        cacheManager.setCacheNames(Arrays.asList("users"));
        return cacheManager;
    }
}

创建实体类和Repository

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;
    
    // 省略getter和setter方法
}

public interface UserRepository extends JpaRepository<User, Long> {
}

创建Service层

在Service层的方法上添加@Cacheable注解:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("执行数据库查询,id: " + id);
        return userRepository.findById(id).orElse(null);
    }
    
    @CacheEvict(value = "users", key = "#id")
    public void deleteUserById(Long id) {
        System.out.println("删除用户,id: " + id);
        userRepository.deleteById(id);
    }
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        System.out.println("更新用户,id: " + user.getId());
        return userRepository.save(user);
    }
}

测试缓存效果

创建测试类,测试缓存效果:

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;
    
    @Test
    public void testCacheable() {
        // 第一次调用,执行方法体并缓存结果
        User user1 = userService.getUserById(1L);
        System.out.println("第一次查询结果:" + user1);
        
        // 第二次调用,直接从缓存获取结果
        User user2 = userService.getUserById(1L);
        System.out.println("第二次查询结果:" + user2);
        
        // 更新用户,触发@CachePut,更新缓存
        user1.setUsername("updated");
        User updatedUser = userService.updateUser(user1);
        System.out.println("更新后的用户:" + updatedUser);
        
        // 第三次调用,从缓存获取更新后的结果
        User user3 = userService.getUserById(1L);
        System.out.println("第三次查询结果:" + user3);
        
        // 删除用户,触发@CacheEvict,清除缓存
        userService.deleteUserById(1L);
        
        // 第四次调用,缓存已被清除,执行方法体
        User user4 = userService.getUserById(1L);
        System.out.println("第四次查询结果:" + user4);
    }
}

测试输出结果如下:

执行数据库查询,id: 1
第一次查询结果:User{id=1, username='test', email='test@example.com'}
第二次查询结果:User{id=1, username='test', email='test@example.com'}
更新用户,id: 1
更新后的用户:User{id=1, username='updated', email='test@example.com'}
第三次查询结果:User{id=1, username='updated', email='test@example.com'}
删除用户,id: 1
执行数据库查询,id: 1
第四次查询结果:null

从测试结果可以看出,缓存注解成功生效:第一次查询执行了方法体,第二次查询直接从缓存获取结果,更新操作触发了缓存更新,删除操作触发了缓存清除,第四次查询因缓存已被清除而再次执行方法体。

缓存抽象的优势与注意事项

优势

  1. 简化缓存代码:通过注解方式实现缓存,避免了手动编写缓存逻辑的繁琐工作。
  2. 统一缓存接口:无论使用何种缓存实现(如Redis、Caffeine等),都通过统一的接口进行操作,降低了更换缓存实现的成本。
  3. 灵活的缓存策略:支持多种缓存键生成方式、缓存条件控制、过期时间设置等,满足不同的业务需求。
  4. 与Spring生态无缝集成:可以与Spring Boot、Spring Data等框架无缝集成,进一步简化开发。

注意事项

  1. 缓存键的唯一性:确保缓存键的唯一性,避免不同方法或参数组合生成相同的缓存键,导致缓存数据混乱。
  2. 缓存穿透问题:当查询一个不存在的数据时,缓存不会命中,导致每次请求都访问数据库。可以通过缓存空值或使用布隆过滤器来解决。
  3. 缓存击穿问题:热点数据的缓存过期时,大量并发请求同时访问数据库。可以通过设置sync = true或使用互斥锁来解决。
  4. 缓存雪崩问题:大量缓存同时过期,导致大量请求同时访问数据库。可以通过设置不同的缓存过期时间或使用熔断降级机制来解决。
  5. 缓存与数据库一致性:在更新或删除数据时,需要及时更新或清除对应的缓存,确保缓存数据与数据库数据一致。

总结

Spring Framework的缓存抽象为开发者提供了一种简单、灵活、高效的缓存解决方案。通过CacheManager接口可以方便地管理各种缓存实现,而@Cacheable注解则极大地简化了缓存逻辑的编写。本文详细介绍了CacheManager的核心作用、常用实现以及@Cacheable注解的属性和使用方法,并通过实际案例演示了缓存抽象的应用。在实际项目中,开发者应根据业务需求选择合适的缓存实现和缓存策略,充分发挥缓存的性能优化作用,同时注意避免缓存相关的常见问题。

官方文档:framework-docs/modules/ROOT/pages/core.adoc 缓存抽象源码:spring-context/src/main/java/org/springframework/cache/

【免费下载链接】spring-framework 【免费下载链接】spring-framework 项目地址: https://gitcode.com/gh_mirrors/spr/spring-framework

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值