SpringCache

日常开发中,对数据进行缓存是优化性能极为重要的一环。Java可以使用的缓存组件有很多,比如JDK自带并发工具包中的ConcurrentMap,或是基于JVM内存的EhCache,以及后来流行起来的缓存中间件MemcachedRedis等。我们直接使用这些组件实现数据缓存,要不胜其烦的编写很多代码,比如“判断缓存中是否存在数据,存在则取缓存,不存在则查数据库并写入缓存”这样的逻辑。

SpringCache则对缓存做了一个封装,并支持注解配置,使用非常方便,一些最简单的缓存逻辑我们可以使用该功能来极大的简化代码,我们这里以SpringBoot工程为例简单了解一下。

缓存相关注解

SpringCache提供了几个注解封装了不同的缓存组件,使用非常方便。

@Cacheable:先判断缓存是否存在,如果缓存存在直接返回缓存,如果缓存不存在则将方法执行后的结果添加到缓存。

@CachePut:执行方法后,将方法执行后的结果更新到缓存。

@CacheEvict:执行方法后移除指定key的缓存。

这些注解可以标注在方法上,它们可以单独使用也可以同时使用。以上方法常用的参数有valuekey

  • value:缓存命名空间,一个命名空间对应一组缓存,该参数是必传的。
  • key:缓存键,可以使用固定字符串,也可以使用SpEL表达式指定,可以指定为操作的数据的主键。如果不指定,SpringCache会根据方法参数值生成缓存键。
  • condition:缓存逻辑执行的条件,使用SpEL进行条件判断。

@Caching:用于组合以上操作。

引入依赖和框架配置

SpringCache提供了SpringBoot的起步依赖,我们将其引入。

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

此外,还需要在application.properties配置文件中配置缓存类型。

spring.cache.type=redis

这里我们需要根据实际使用的缓存底层实现选择,后文会介绍集成EhCache和Redis的方法,但要注意不同的缓存底层实现可能在配置方法上有区别。

最后,我们需要在工程的启动类上添加@EnableCaching注解配置开启缓存。

package com.gacfox.democache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

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

集成EhCache

对于单机应用,使用内存作为缓存性能相比Redis更好,EhCache是一个比较常用的内存缓存框架。如果使用EhCache缓存,我们需要引入EhCache的依赖。在SpringBoot工程中,该依赖的版本由SpringBoot基础工程管理,我们可以不指定版本号。

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

注意:EhCache2.x的groupId是net.sf.ehcache,3.x的groupId是org.ehcache,对于SpringBoot2.x我们需要引入2.x的EhCache,不要写错groupId搞错版本,新版本实测是未兼容的。

application.properties配置中,我们需要配置缓存类型为ehcache,此外还需要指定ehcache.xml配置文件的路径。

spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml

ehcache.xml配置文件内容例子如下。

<?xml version="1.0" encoding='UTF-8'?>
<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <cache
            name="users"
            maxElementsInMemory="1024"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            diskPersistent="false"
    />
</ehcache>

ehcache.xml配置文件中,我们需要根据实际需要配置缓存的命名空间、大小、超时等参数。

此时SpringCache缓存就可以正常工作了。

集成Redis

SpringCache集成Redis非常简单,我们需要引入SpringDataRedis依赖。

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

引入依赖后,配置SpringCache缓存类型为redis即可。此外,这里也可以对缓存的键前缀、超时时间等进行配置。

spring.cache.type=redis
spring.cache.redis.time-to-live=120s
spring.cache.redis.use-key-prefix=true
spring.cache.redis.key-prefix=CACHE_

缓存方法返回值

下面例子中,我们使用@Cacheable缓存了queryUserList()方法的查询结果。该方法第一次调用时会真实执行内部的代码,但随后的调用中,在缓存过期之前,都会返回缓存的数据。

@Slf4j
@Service("demoService")
public class DemoService {
    @Cacheable("users")
    public List<User> queryUserList() {
        // ... 具体的业务逻辑
        return users;
    }
}

如果方法存在参数,缓存键会自动根据方法参数生成,缓存键可以简单理解为Redis的Key,它实际上就是一个用来判断缓存命中和取缓存值的字符串。下面例子中,缓存会根据参数id进行判断,相同参数的查询会生成相同的缓存键,因此会返回同一个缓存。

@Slf4j
@Service("demoService")
public class DemoService {
    @Cacheable("users")
    public User queryUserById(Long id) {
        // ... 具体的业务逻辑
        return user;
    }
}

对于缓存键我们也可以手动指定,下面例子使用SpEL指定了key参数。

@Cacheable(value = "users", key = "#id")

SpEL是一种表达式,其中可以包含字符串拼接、计算等逻辑,下面是一个例子。

@Cacheable(value = "district", key = "#cityCode + '_' + #districtName")
public District getAreaByName(String cityCode, String districtName) { }

手动清除缓存

默认情况下,缓存会在超时后删除,但有时我们也需要手动让缓存失效,此时可以使用@CacheEvict。下面例子代码中,我们对queryUserList()方法进行了缓存,它使用ALL作为键。而执行updateUsers()方法会导致ALL键缓存的清除。

@Slf4j
@Service("demoService")
public class DemoService {
    @Cacheable(value = "users", key = "'ALL'")
    public List<User> queryUserList() {
        // ... 具体的业务逻辑
        return users;
    }

    @CacheEvict(value = "users", key = "'ALL'")
    public void updateUsers() {
        // ... 具体的业务逻辑
    }
}

注意:这里的字符串key = "'ALL'"表示名字叫ALL的缓存键,key属性的值是SpEL表达式,因此字符串ALL外我们需要增加单引号。

默认情况下,@CacheEvict在方法执行后移除缓存,我们也可以指定参数beforeInvocation = true指定在方法执行前移除缓存。

编程式操作缓存

除了使用SpringCache提供的注解,我们也可以通过代码的方式读写缓存,这需要使用SpringCache的CacheManager接口。项目中配置SpringCache的起步依赖后,我们可以通过依赖注入的方式获取配置好的缓存实现,下面例子中,我们注入CacheManager实现并读取缓存。

package com.gacfox.demo.service;

import com.gacfox.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Slf4j
@Service("demoService")
public class DemoService {
    @Resource
    private CacheManager cacheManager;

    public User queryUserById(Long id) {
        Cache cache = cacheManager.getCache("user_cache");
        if (cache != null) {
            return cache.get(id, User.class);
        } else {
            return null;
        }
    }
}

代码中,我们先通过CacheManager实现获取了Cache实例,它需要一个缓存名,这和@Cacheablevalue作用是相同的。获取Cache实例后,我们就可以通过get()put()evict()clear()等方法操作缓存了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。