美文网首页
【每天学点Spring】Spring Cache入门,与Spri

【每天学点Spring】Spring Cache入门,与Spri

作者: 伊丽莎白2015 | 来源:发表于2022-12-23 11:30 被阅读0次

【本文内容】

  • Spring Cache + Spring Boot集成,用默认的基于内存的Simple cache作为缓存实现。
  • 基于Redis的缓存实现。
  • 介绍5个重要的注解:@Cacheable, @CacheEvict, @CachePut, @Caching, @CacheConfig
  • 基于Hazelcast的缓存实现,如果同时依赖Hazelcast和Redis,自定义RedisCacheManager来指定使用Redis作为缓存实现。

【参考】

【文档】


从Spring 3.1开始,Spring框架提供了关于缓存的抽象接口来统一不同的缓存技术(如Redis, EhCache, Hazelcast等)。Spring Cache 只负责维护抽象层,具体的实现可以自主选择。

1. 简单的例子

1.1 依赖

Spring Cache主要位于spring-contextspring-context-support包中,但Spring Boot整合成相关的starter。我用的是Spring Boot v2.7.0。除了必要的jpa等依赖,需要加入:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
1.2 启用Cache

使用注解@EnableCaching来表示使用Spring Cache:

@EnableCaching
@SpringBootApplication
public class SpringCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringCacheApplication.class, args);
    }
}

如果需要关闭,可以在application.properties中加(这个优先级高于上述的@EnableCaching):

spring.cache.type=none
1.3 Service

Course以及CourseRepository就是普通的JPA相关的类,这里略。

@Autowired
    private CourseRepository courseRepository;

    @Cacheable("courses")
    public Course getById(int id) {
        System.out.println("get from db for id = " + id);
        Optional<Course> courseOptional = courseRepository.findById(id);
        return courseOptional.isPresent() ? courseOptional.get() : null;
    }
1.4 配置Cache的实现

由于Spring Cache提供的是抽象层,并没有具体的实现,所以需要我们配置相应的实现。

Spring Cache主要是通过CacheManager来操作Cache的put/get等,所以需要配置一个这样的Bean,如果没有配置,在Spring Boot Starter中会找classpath中有具体的实现,在Spring Boot AutoConfigure阶段会自动配置一个默认的。

具体支持的Cache实现有:

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

如果在classpath中没有找到具体的Cache实现,那么就会用ConcurrentHashMap来实现简单的Cache。

比如我们写个测试用例:

@SpringBootTest
public class CourseServiceTest {
    @Autowired
    private CourseService courseService;

    @Autowired
    private CacheManager cacheManager;

    @Test
    public void getByIdTest() {
        cacheManager.getCacheNames().stream().forEach(System.out::println);
        System.out.println(courseService.getById(1));

        cacheManager.getCacheNames().stream().forEach(System.out::println);
        System.out.println(courseService.getById(1));
    }
能过debug可以看到由于没有配置classpath,会默认生成一个ConcurrentMapCacheManager的CacheManager的Bean: image.png

打印结果:可以看到标注了@Cacheable的方法,在查询的时候,第一次会从数据库查,第二次直接从Cache里拿的:

image.png

【至此,基于内存的Simple Cache实现 + Spring Cache + Spring Boot的例子完毕。】


2. 改为Redis作为Cache的实现

使用Docker来启动Redis以及与Spring Boot的集成,参考我之前的文章:https://www.jianshu.com/p/7877ecc6a913

在classpath中加入了redis相关的依赖后:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
Debug cacheManager的bean,可以看到会从默认的ConcurrentMapCacheManager改为基于Redis的RedisCacheManager的bean: image.png

3. 一些重要的注解

  • @Cacheable: 查找缓存:有就返回;没有就执行方法体,将结果缓存起来。
  • @CacheEvict: 清除缓存。
  • @CachePut: 执行方法体,将结果缓存起来。
  • @Caching: 设置方法的复杂缓存规则。
  • @CacheConfig: 抽取类中的所有@CachePut, @Cacheable, @CacheEvict的公共配置。
3.1 @Cacheable

这个注解在上述#1.3中有配,标记在方法上,需要写上cacheNames(可以是多个)。如:

@Cacheable("courses")
    public Course getById(int id) {...}

getById(int)方法会先从缓存中查找是否有相应的结果,如果没有,才会真正的执行该方法,并且在结束后将结果放入缓存中。

如使用的Cache实现为Redis,可以看到上述@Cacheable的运行后,在Redis中存的方式是配置的name + "::" + key(默认的key为传入的参数)

image.png 因为我没有使用Json存储,而是单纯的序列化,看起来有点费劲: image.png

【其它】
还可以加个条件,比如id值大于等于2的时候才会存入cache:

 @Cacheable(value = "courses", condition = "#id >= 2")
可以看到,同时查询id=1,2的时候,只有id=2的结果数据存入了Redis中: image.png

可以改变上述的默认用参数作为key的存储方式,比如:

@Cacheable(value = "courses", key = "{#root.methodName, #id}")

这样存储的key就会变为配置的name + "::" + 方法名 + "," + 参入的参数。

3.2 @CacheEvict

该注解的作用是按需清除缓存,比如数据更新了,数据删除了等。

    @CacheEvict("courses")
    public void deleteById(int id) {
        courseRepository.deleteById(id);
    }

测试用例:

    @Test
    public void deleteByIdTest() {
        courseService.deleteById(2);
    }

方法执行前缓存中有两个key,执行后会清除key为courses::2的数据:

image.png

也可以使用allEntries=true来清理所有的cache。

    @CacheEvict(value = "courses", allEntries = true)
    public boolean clearAllCache() {
        return true;
    }

测试用例:

    @Test
    public void clearAllCacheTest() {
        courseService.clearAll();
    }
可以看到缓存中的以courses命令的cache被全部删除了: image.png
3.3 @CachePut

该注解有点像@Cacheable,只是不会先从缓存读取,而是会执行方法体,将结果缓存起来。

    @CachePut("courses")
    public Course update(Course course) {
        Course courseInDb = courseRepository.findById(course.getId()).orElseThrow();
        courseInDb.setName(course.getName());
        courseInDb.setStatus(course.getStatus());
        return courseRepository.save(courseInDb);
    }
测试前: image.png

测试用例,更新两次:

    @Test
    public void updateCourse() {
        Course course = new Course();
        course.setId(2);
        course.setName("test updated");
        course.setStatus("0");
        courseService.update(course);

        Course course1 = new Course();
        course1.setId(2);
        course1.setName("test updated111");
        course1.setStatus("0");
        courseService.update(course1);
    }

可以看到如同上述描述的,@CachePut每次都会进入方法体,并且会把执行结果存到缓存中:

image.png

但我们的例子有个问题,即update更新的cache,key和getById不一样,所以我们可以改下:

    @CachePut(value = "courses", key = "#course.id")
重新跑测试用例,可以看到key改成默认的id了: image.png
3.4 @Caching

在同一个方法上想要同时使用多个Cache相关的注解,可以使用@Caching注解来将它们gourp起来。如:

@Caching(evict = {
    @CacheEvict(cacheNames = "departments", allEntries = true), 
    @CacheEvict(cacheNames = "employees", key = "...")})
public boolean importEmployees(List<Employee> data) { ... }
3.5 @CacheConfig

@CacheConfig允许我们将cache的配置放在class级别,这样就可以不同在每个方法中都重复声明,比如上述的CourseService,我们同时对getById, update, deleteById都声明了value=courses(这里的value等同于cacheNames),那么可以直接在CourseService类上声明:
类级别注解,用于配置一些共同的选项(当方法注解声明的时候会被覆盖),例如CacheName。

@CacheConfig(cacheNames = "courses")
public class CourseServiceImpl implements CourseService {

    @Cacheable
    public Course getById(int id) {...}

    @CachePut(key = "#course.id")
    public Course update(Course course) {...}
}

如果方法级别上也有声明,那么优先级高于类级别上的声明。

4. 与Hazelcast集成

关于Spring与Hazelcast的集成,请参考我之前的文章:https://www.jianshu.com/p/913f35f41f31

这里假设已经配好Hazelcast的依赖以及hazelcast.xml

Debug cacheManager,可以看到CacheManager的实现为:HazelcastCacheManager

image.png

如果我把redis和hazelcast同时放在classpath下,那么想要Spring Cache知道具体用哪个Cache实现,可以自定义CacheManager,而不是依赖Spring Boot AutoConfigure来创建bean,比如自定义RedisCacheManager

@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
}

这样即便classpath下同时存在hazelcast和redis,也会始终使用redis作为Cache实现。

相关文章

网友评论

      本文标题:【每天学点Spring】Spring Cache入门,与Spri

      本文链接:https://www.haomeiwen.com/subject/dmsmqdtx.html