【每天学点Spring】Spring Cache入门,与Spring Boot集成

【本文内容】

  • 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实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容