一、前言

在程序中可以使用缓存的技术来节省对数据库的开销。Spring Boot对缓存提供了很好的支持,我们几乎不用做过多的配置即可使用各种缓存实现。这里主要介绍平日里个人接触较多的Ehcache和Redis缓存实现。

二、准备工作

可根据 《Spring Boot整合MyBatis》 搭建一个Spring Boot项目,然后yml中配置日志输出级别以观察SQL的执行情况:

1
2
3
4
5
6
logging:
level:
com:
wno704:
boot:
mapper: DEBUG

其中com.spring.mapper为MyBatis的Mapper接口路径。

三、整合EhCache 缓存

3.1 添加依赖

1
2
3
4
5
6
7
8
9
10
        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- spring-boot ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

3.2 添加配置

1)在 src/main/resources 目录下创建 ehcache.xml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir/ehcache"/>

<!-- 默认缓存 -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>

<!-- 自定义缓存 -->
<cache name="student"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="50"
timeToLiveSeconds="50"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>

说明:

1
2
3
4
5
6
7
8
9
10
11
12
    name:Cache 的唯一标识
maxElementsInMemory:内存中允许存储的最大的元素个数
maxElementsOnDisk:硬盘最大缓存个数,0代表无限个
clearOnFlush:内存数量最大时是否清除
eternal:缓存对象是否永久有效,如果是,超时设置将被忽略
overflowToDisk:内存不足(超过 maxElementsInMemory)时,是否启用磁盘缓存
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds:缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间
diskPersistent:是否将缓存数据持久化到磁盘上,如果为 true,JVM 重启数据依然存在。默认值是false
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒
memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将根据指定策略清除内存。默认为 LRU(最近最少使用),其他策略有 FIFO(先进先出),LFU(较少使用)

2)修改application.yml :

1
2
3
4
5
6
spring:
cache:
#缓存类型(ehcache、redis)
type: ehcache
ehcache:
config: classpath:ehcache.xml

3)接着在Spring Boot入口类中加入@EnableCaching注解开启缓存功能:

1
2
3
4
5
6
7
@EnableCaching
@SpringBootApplication
public class EhcacheApplication {
public static void main(String[] args) {
SpringApplication.run(EhcacheApplication.class, args);
}
}

3.3 编码

《Spring Boot整合MyBatis》 的基础上,结合 Mybatis 测试:
Service 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@CacheConfig(cacheNames = "student")
public interface StudentService {
@CachePut(key = "#p0.sno")
Student save(Student student);

@CachePut(key = "#p0.sno")
Student update(Student student);

@CacheEvict(key = "#p0", allEntries = true)
void deleteStudentBySno(int sno);

@Cacheable(key = "#p0")
Student queryStudentBySno(int sno);
}

我们在StudentService接口中加入了@CacheConfig注解,queryStudentBySno方法使用了注解@Cacheable(key="#p0"),即将id作为redis中的key值。当我们更新数据的时候,应该使用@CachePut(key="#p0.sno")进行缓存数据的更新,否则将查询到脏数据,因为该注解保存的是方法的返回值,所以这里应该返回Student。

3.4 测试

由于 ehcache 缓存是存储在应用的内存中,如果使用 junit 测试,方法执行完毕缓存就释放了,无法正常测试缓存效果,因此测试使用发起 http 请求的形式。

发起查询请求:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test1() throws Exception {
Student student1 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student1.getSno() + "的学生姓名为:" + student1.getName());

Student student2 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student2.getSno() + "的学生姓名为:" + student2.getName());

Student student3 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student3.getSno() + "的学生姓名为:" + student3.getName());
}

结果:

1
2
3
4
5
6
2020-08-18 11:13:21.524 DEBUG 3156 --- [           main] c.w.b.m.StudentMapper.queryStudentBySno  : ==>  Preparing: select * from student where sno=? 
2020-08-18 11:13:21.654 DEBUG 3156 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : ==> Parameters: 1(Integer)
2020-08-18 11:13:21.679 DEBUG 3156 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : <== Total: 1
学号1的学生姓名为:Mikle
学号1的学生姓名为:Mikle
学号1的学生姓名为:Mikle

发起修改请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test2() throws Exception {
Student student1 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student1.getSno() + "的学生姓名为:" + student1.getName());

student1.setName("康康");
this.studentService.update(student1);

Student student2 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student2.getSno() + "的学生姓名为:" + student2.getName());

Student student3 = this.studentService.queryStudentBySno(1);
System.out.println("学号" + student3.getSno() + "的学生姓名为:" + student3.getName());
}

修改成功后,立刻发起查询请求,没有日志打印,但返回修改后的对象数据,说明缓存中的数据已经同步。

1
2
3
4
5
6
7
8
9
10
11
12
2020-08-18 11:15:15.992 DEBUG 24212 --- [           main] c.w.b.m.StudentMapper.queryStudentBySno  : ==>  Preparing: select * from student where sno=? 
2020-08-18 11:15:16.129 DEBUG 24212 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : ==> Parameters: 1(Integer)
2020-08-18 11:15:16.155 DEBUG 24212 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : <== Total: 1
学号1的学生姓名为:Mikle
2020-08-18 11:15:16.163 DEBUG 24212 --- [ main] c.w.boot.mapper.StudentMapper.update : ==> Preparing: update student set sname=?,ssex=? where sno=?
2020-08-18 11:15:16.166 DEBUG 24212 --- [ main] c.w.boot.mapper.StudentMapper.update : ==> Parameters: 康康(String), M(String), 1(Integer)
2020-08-18 11:15:16.171 DEBUG 24212 --- [ main] c.w.boot.mapper.StudentMapper.update : <== Updates: 1
2020-08-18 11:15:16.172 DEBUG 24212 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : ==> Preparing: select * from student where sno=?
2020-08-18 11:15:16.172 DEBUG 24212 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : ==> Parameters: 1(Integer)
2020-08-18 11:15:16.173 DEBUG 24212 --- [ main] c.w.b.m.StudentMapper.queryStudentBySno : <== Total: 1
学号1的学生姓名为:康康
学号1的学生姓名为:康康

四、整合Redis缓存:

4.1 添加依赖

1
2
3
4
5
6
7
8
9
10
        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

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

4.2 配置

1)修改application.yml:

1
2
3
4
5
6
7
8
spring:
redis:
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 10007
# Redis服务器连接密码
password:

2)接着在Spring Boot入口类中加入@EnableCaching注解开启缓存功能:

1
2
3
4
5
6
7
@EnableCaching
@SpringBootApplication
public class EhcacheApplication {
public static void main(String[] args) {
SpringApplication.run(EhcacheApplication.class, args);
}
}

五、缓存注解

  1. @CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "student"):配置了该数据访问对象中返回的内容将存储于名为student的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义;

  2. @Cacheable:配置了queryStudentBySno函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:

  • value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了;
  • key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0"):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考 https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
  • condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存;
  • unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断;
  • keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定;
  • cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用;
  • cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定;
  1. @CachePut:配置于函数上,能够根据参数定义条件来进行缓存,其缓存的是方法的返回值,它与@Cacheable不同的是,它每次都会真实调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析;

  2. @CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
    allEntries:非必需,默认为false。当为true时,会移除所有数据;
    eforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。

六、缓存实现

要使用上Spring Boot的缓存功能,还需要提供一个缓存的具体实现。Spring Boot根据下面的顺序去侦测缓存实现:
Generic->JCache (JSR-107)->EhCache 2.x->Hazelcast->Infinispan->Redis->Guava->Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。