在数据量比较大时,持续的访问数据库,数据库的性能就成为系统的瓶颈。在读多写少的场景中,可以利用缓存来提高系统的性能。
在Spring Boot中对于缓存的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。
Cache
Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。
- CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
- CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache 存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
- Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个 CacheManager所拥有。
- Entry是一个存储在Cache中的key-value对。
- Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期 的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
Spring 缓存
Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术; 并支持使用JCache(JSR-107)注解简化我们开发。
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。
Cache接口下Spring提供了各种xxxCache的实现;如 RedisCache,EhCacheCache , ConcurrentMapCache。
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法 并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、 ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
@Cacheable/@CachePut/@CacheEvict 主要的参数
value | 缓存的名称,在 spring 配置文件中定义,必须指定 至少一个 | 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达 式编写,如果不指定,则缺省按照方法的所有参数 进行组合 | 例如: @Cacheable(value=”testcache”,key=”#userName” |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在 调用方法之前之后都能判断 | 例如: @Cacheable(value=”testcache”,condition=”#userNam e.length()>2”) |
allEntries (@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | 例如:@CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation (@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定 为 true,则在方法还没有执行的时候就清空缓存, 缺省情况下,如果方法执行抛出异常,则不会清空 缓存 | 例如:@CachEvict(value=”testcache”, beforeInvocation=true) |
unless (@CachePut) (@Cacheable) | 用于否决缓存的,不像condition,该表达式只在方 法执行之后判断,此时可以拿到返回值result进行判 断。条件为true不会缓存,fasle才缓存 | 例如:@Cacheable(value=”testcache”,unless=”#result == null”) |
Cache SpEL available metadata
名字 | 位置 | 描述 | 实例 |
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache。 | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的 形式,0代表参数的索引。 | #a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如 ‘unless’,’cache put’的表达式 ’cache evict’的表达式 beforeInvocation=false) | #result |
自动配置原理
spring cache 的自动配置和之前的mongo 一样。
参考:
配置文件添加:
application.properties
debug=true
将 spring 的 debug 结果打印出来:
CacheAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.cache.CacheManager' (OnClassCondition)
- @ConditionalOnBean (types: org.springframework.cache.interceptor.CacheAspectSupport; SearchStrategy: all) found bean 'cacheInterceptor'; @ConditionalOnMissingBean (names: cacheResolver; types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)
CacheAutoConfiguration#cacheManagerCustomizers matched:
- @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers; SearchStrategy: all) did not find any beans (OnBeanCondition)
CacheAutoConfiguration.CacheManagerJpaDependencyConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean' (OnClassCondition)
- @ConditionalOnBean (types: org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; SearchStrategy: all) found bean '&entityManagerFactory' (OnBeanCondition)
-------------------------分割线---------------------------------------
SimpleCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
- @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)
最终注入的是 SimpleCacheConfiguration 这个配置类。
那么我们看一下,为什么会注入 SimpleCacheConfiguration 这个配置类,而不是 CacheAutoConfiguration。
CacheAutoConfiguration
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<CacheManagerCustomizer<?>> customizers) {
return new CacheManagerCustomizers(
customizers.orderedStream().collect(Collectors.toList()));
}
...
}
注意这三个注解:
- @ConditionalOnClass(CacheManager.class)
- @ConditionalOnBean(CacheAspectSupport.class)
- @ConditionalOnMissingBean(value = CacheManager.class, name = “cacheResolver”)
再来看 SimpleCacheConfiguration
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
- @ConditionalOnMissingBean(CacheManager.class)
- @Conditional(CacheCondition.class)
如果没有 Bean CacheManager,那么默认会使用 SimpleCacheConfiguration 这个配置类。
然后会创建一个 ConcurrentMapCacheManager 对象,使用期对 Cache 进行管理。
ConcurrentMapCacheManager
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
private boolean dynamic = true;
private boolean allowNullValues = true;
private boolean storeByValue = false;
@Nullable
private SerializationDelegate serialization;
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
for (String name : cacheNames) {
this.cacheMap.put(name, createConcurrentMapCache(name));
}
this.dynamic = false;
}
else {
this.dynamic = true;
}
}
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
...
}
springboot 使用 cache
pom 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- h2 java 写的内存数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@EnableCaching 开启注解缓存
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Service 层使用 @Cache
@Service
@Slf4j
@CacheConfig
// @CacheConfig主要用于配置该类中会用到的一些共用的缓存配置
// @CacheConfig的作用:抽取@Cacheable、@CachePut、@CacheEvict的公共属性值
// @CacheConfig的属性
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
// 缓存用在更新/插入比较少的情况
@Cacheable(cacheNames = "coffeeList") // 缓存结果
public List<Coffee> getAllCoffee() {
return coffeeRepository.findAll(Sort.by("id"));
}
/*1. @Cacheable的几个属性详解:
* cacheNames/value:指定缓存组件的名字
* key:缓存数据使用的key,可以用它来指定。默认使用方法参数的值,一般不需要指定
* keyGenerator:作用和key一样,二选一
* cacheManager和cacheResolver作用相同:指定缓存管理器,二选一
* condition:指定符合条件才缓存,比如:condition="#id>3"
* 也就是说传入的参数id>3才缓存数据
* unless:否定缓存,当unless为true时不缓存,可以获取方法结果进行判断
* sync:是否使用异步模式*/
// @Cacheable(cacheNames= "coffee")
// @Cacheable(cacheNames= "coffee",key="#name",condition="#name != 'admin'")
@Cacheable(cacheNames = "coffee", key = "#name")
public Coffee getCoffeeByName(String name) {
return coffeeRepository.findCoffeeByName(name);
}
// @CachePut必须结合@Cacheable一起使用,否则没什么意义
// @CachePut的作用:即调用方法,又更新缓存数据 ,修改了数据库中的数据,同时又更新了缓存!
/**
* @CachePut:即调用方法,又更新缓存数据
* 修改了数据库中的数据,同时又更新了缓存
*
*运行时机:
* 1.先调用目标方法
* 2.将目标方法返回的结果缓存起来
*
* 测试步骤:
* 1.查询name的咖啡信息
* 2.以后查询还是之前的结果
* 3.更新name的咖啡信息
* 4.查询name=?返回的结果是什么?
* 应该是更新后的咖啡信息
* 但只更新了数据库,但没有更新缓存是什么原因?
* 5.如何解决缓存和数据库同步更新?
* 这样写:@CachePut(cacheNames = "coffee",key = "#coffee.name")
* @CachePut(cacheNames = "coffee",key = "#result.name")
*/
@CachePut(cacheNames = "coffee", key = "#result.name")
public Coffee updateCoffee(Coffee coffee) {
return coffeeRepository.save(coffee);
}
/**
* @Caching是 @Cacheable、@CachePut、@CacheEvict注解的组合
* 以下注解的含义:
* 1.当使用指定名字查询数据库后,数据保存到缓存
* 2.现在使用id、name就会直接查询缓存,而不是查询数据库
*/
@Caching(
cacheable = {@Cacheable(cacheNames="coffee",key="#name")},
put={ @CachePut(key = "#result.id"),
@CachePut(key = "#result.name")
}
)
public Coffee queryCoffeeByName(String name){
return coffeeRepository.findCoffeeByName(name);
}
/**
* @CacheEvict:清除缓存
* 1.key:指定要清除缓存中的某条数据
* 2.allEntries=true:删除缓存中的所有数据
* beforeInvocation=false:默认是在方法之后执行清除缓存
* 3.beforeInvocation=true:现在是在方法执行之前执行清除缓存,
* 作用是:只清除缓存、不删除数据库数据
*/
//@CacheEvict(cacheNames = "coffee",key = "#name")
@CacheEvict(cacheNames = "coffee")
public void delete(String name) {
};
@CacheEvict(cacheNames = "coffeeList")
public void reload() {
}
public List<Coffee> getCoffeeByName(List<String> names) {
return coffeeRepository.findByNameInOrderById(names);
}
}
Controller 层调用 service 层的方法
@Controller
@RequestMapping("/coffee")
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@GetMapping("/list")
@ResponseBody
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
@GetMapping("/get/{name}")
@ResponseBody
public Coffee getCoffeeByName(@PathVariable(value="name") String name){
return coffeeService.getCoffeeByName(name);
}
@PostMapping("/update")
@ResponseBody
public Coffee updateCoffeeByName(@RequestBody Coffee coffee){
return coffeeService.updateCoffee(coffee);
}
@GetMapping("/delete/{name}")
@ResponseBody
public String deleteCoffeeByName(@PathVariable(value="name") String name){
coffeeService.delete(name);
return "success";
}
@GetMapping("/reload")
@ResponseBody
public String delete(){
coffeeService.reload();
return "success";
}
}
测试
使用 vscode restClient 插件测试:
### GET
GET http://localhost:8080/coffee/list
### GET
GET http://localhost:8080/coffee/reload
### GET
GET http://localhost:8080/coffee/get/latte
### GET
GET http://localhost:8080/coffee/delete/latte
### Coffee Post
POST http://localhost:8080/coffee/update
Content-Type: application/json
{
"id": 2,
"name": "latte",
"price": 1000
}