一个系统的瓶颈通常在与数据库交互的过程中。当我们重复地获取相同的数据的时候,一次又一次的请求数据库或者远程服务,这无疑是性能上的浪费——会导致大量的时间耗费在数据库查询或者远程方法调用上,导致程序性能恶化。缓存(Cache)作为数据交换的缓冲区,是应用系统性能提升的大杀器。内存的读写速度通常是硬盘的几十倍到上百倍,缓存实际上就是把热点数据保存在内存中,利用内存的高速读写特性提高热点数据的操作速度。
Spring Boot 中使用缓存非常简单,并且支持多种实现缓存的方式。本章讨论比较常用的几种缓存实现方式,及其对应的场景。
15.1 Spring Boot 默认缓存
Spring Boot 默认缓存是基于 ConcurrenMapCacheManager
缓存管理器实现的,从这个类名就能发现它本质上应该是一个 Map 集合容器。它的结构比较简单,一般用于比较轻量级的缓存使用场景。也就是缓存的数据量比较小,缓存操作不是特别频繁的场景。本节简单介绍默认缓存的使用方法。
15.1.1 引入项目依赖
我们引入如下的缓存依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
15.1.3 缓存的注解说明
@CacheConfig
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {“cacheName”})注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
sync | 是否使用异步模式,默认为false |
@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上 查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
allEntries | 是否清空所有缓存,默认为 false。如果true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果 true,则在方法执行前就会清空缓存 |
@Caching
15.2 设计示例代码
15.2.1 开启缓存
在启动类上添加注解 @EnableCaching 开启缓存功能。
@EnableCaching
@SpringBootApplication
public class CloudApplication {
15.2.2 示例数据对象
package com.longser.union.cloud.data.model;
import lombok.Data;
import java.io.Serializable;
@Data
public class Goods implements Serializable {
private static final long serialVersionUID = 1L;
long id;
String name;
}
这里一定要实现 Serializable 接口
15.2.3 数据访问的模拟代码
正常服务层方法会调用数据访问层方法访问数据库,此处我们只需要演示缓存的作用,所以打印日志代替数据库访问方法。
package com.longser.union.cloud.service;
import com.longser.union.cloud.data.model.Goods;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* 商品服务类
* @author David
*
* 注解 @CacheConfig 注解用于指定本类中方法使用的缓存名称,该类使用的缓存名称为 GoodsCache ,
* 与其他缓存区域是隔离的。
*/
@Service
@CacheConfig(cacheNames = "GoodsCache")
public class GoodsService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 按id获取商品信息
*
* 注解 @Cacheable 用于开启方法缓存,缓存的键是方法的参数,缓存的值是方法的返回值。
* 如果多次调用该方法时参数 id 值相同,则第一次会执行方法体,并将返回值放入缓存;
* 后续方法不会再执行方法体,直接将缓存的值返回。
*/
@Cacheable
public Goods getById(Long id) {
logger.info("getById({})", id);
Goods goods = new Goods();
goods.setId(id);
goods.setName("goods-" + id);
return goods;
}
/**
* 删除商品
*
* 注解 @CachePut 可以更新缓存,key = "#id" 表示采用参数中的 id 属性作为键。
* 当缓存中该键的值不存在时,则将返回值放入缓存;当缓存中该键的值已存在时,
* 会更新缓存的内容。
*/
@CacheEvict(key = "#id")
public void remove(Long id) {
logger.info("remove({})", id);
}
/**
* 编辑商品信息
*
* 注解 @CacheEvict 可以移除缓存,当调用该方法时,会移除 goods 中 id 属性对应的缓存内容。
*/
@CachePut(key = "#goods.id")
public Goods edit(Goods goods) {
logger.info("edit id:{}", goods.getId());
return goods;
}
}
15.2.4 测试默认缓存
为了充分理解缓存的含义,我们通过测试类发起测试。
package com.longser.union.cloud.cache;
import com.longser.union.cloud.data.model.Goods;
import com.longser.union.cloud.service.GoodsService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;
@SpringBootTest
public class CacheTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private CacheManager cacheManager;
@Autowired
private GoodsService goodsService;
// 显示当前使用的缓存管理器类型
@Test
public void showCacheManager() {
// 输出当前的缓存管理器名称
// 默认Cache值为 org.springframework.cache.concurrent.ConcurrentMapCacheManager
// JCache 值为 org.springframework.cache.jcache.JCacheCacheManager
System.out.println("Cache Manager: " + cacheManager.getClass().toString());
}
// 缓存测试
@Test
public void cacheTest() {
// 第一次执行,没有缓存,执行方法体
goodsService.getById(1L);
// 再次执行,直接取出缓存,不执行方法体
goodsService.getById(1L);
// 第一次执行,没有缓存,执行方法体
goodsService.getById(8L);
// 移除缓存
goodsService.remove(1L);
// 再次执行,已经没有对应缓存,所以执行方法体
Goods oldGoods = goodsService.getById(1L);
// 打印缓存内容
logger.info("old goods id:{} name:{}", oldGoods.getId(), oldGoods.getName());
// 更新缓存
Goods temp = new Goods();
temp.setId(1L);
temp.setName("新的商品");
goodsService.edit(temp);
// 查询并打印已更新的缓存内容
Goods newGoods = goodsService.getById(1L);
logger.info("new goods id:{} name:{}", newGoods.getId(), newGoods.getName());
}
}
showCacheManager 的输出是
Cache Manager: class org.springframework.cache.concurrent.ConcurrentMapCacheManager
15.3 使用 Ehcache 缓存
Spring Boot 默认的缓存实现比较简单,功能也十分有限。如果是企业级的中大型应用,需要寻求更加稳定、可靠的缓存框架。
Ehcache 是 Java 编程领域非常著名的缓存框架,具备两级缓存数据——内存和磁盘,因此不必担心内存容量问题。另外 Ehcache 缓存的数据会在 JVM 重启时自动加载,不必担心断电丢失缓存的问题。
Ehcache 的功能完整性和运行稳定性远远强于 Spring Boot 默认的缓存实现方式,而且 Spring Boot 使用 Ehcache 非常便捷,它可以直接使用或者做为 JCache 的 Provider。接下来我们就后者的方式来实现缓存,它的好处是只需要完成简单的配置,代码中已经有的那些调用 JSR 107 标准 JCache API 的代码完全不需要修改。
15.3.1 添加 Ehcache 依赖
在 spring-boot-cache 项目的基础上添加 Ehcache 依赖。
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.7</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
</dependency>
<!-- Runtime, com.sun.xml.bind module -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>
java.xml.bind 和 com.sun.xml.bind 本来是包含在 JDK 8中的,但 JDK11 开始不再包括他们,因此这里需要专门设置依赖。
15.3.2 添加 Ehcache 配置文件
首先在 application.yaml 中指定配置文件的位置。
spring:
cache:
type: jcache
jcache:
config: classpath:config/ehcache.xml
然后在 resource/ 文件夹中添加 ehcache.xml 配置文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
<!-- 持久化路径 -->
<persistence directory="~/" />
<!--缓存模板 -->
<cache-template name="CacheTemplate">
<expiry>
<!--存活时间 -->
<tti>60</tti>
</expiry>
<resources>
<!--堆空间 -->
<heap unit="entries">2000</heap>
<!-- 堆外空间 -->
<offheap unit="MB">500</offheap>
</resources>
</cache-template>
<!--缓存对象 -->
<cache alias="GoodsCache" uses-template="CacheTemplate">
</cache>
</config>
Ehcache 的配置比较复杂,此处只是给出简单的示例,感兴趣的可以查阅更多资料。
15.3.3 测试Ehcache缓存
无须额外配置,再次运行测试代码。cacheTest()
的运行结果和之前的一致,而showCacheManager()
输出的缓存管理器名称应该为 **org.springframework.cache.jcache.JCacheCacheManager**
。
15.4 小结
Spring Boot 支持多种缓存实现方式,可以根据项目需求灵活选择。
- 缓存数据量较小的项目,可以使用 Spring Boot 默认缓存。
- 缓存数据量较大的项目,可以考虑使用 Ehcache 缓存框架。
- 如果是大型系统,对缓存的依赖性比较高,应该采用下一章讨论的方式,使用独立的缓存组件 Redis ,通过主备、集群等形式提高缓存服务的性能和稳定性。
版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。