Spring Boot————默认缓存应用及原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014745069/article/details/86438487

引言

应用程序的数据除了可以放在配置文件中、数据库中以外,还会有相当一部分存储在计算机的内存中,这部分数据访问速度要快于数据库的访问,因此通常在做提升数据访问速度时,会将需要提升访问速度的数据放入到内存中,我们称之为缓存

最常用的缓存方式是使用并发容器,因为具有比较高的并发性能,因此Spring的默认缓存策略就是使用ConcurrentHashMap作为缓存容器。下面将会逐步展开缓存的概念与Spring中的使用规则。

一、JSR-107缓存API

为了统一缓存的开发规范,以及提升系统的扩展性,J2EE发布了JSR-107缓存规范。主要定义了五大核心接口:

CachingProvider、CacheManager、Cache、Entry、Expiry

而实际开发中,我们通常会使用Spring缓存抽象来完成对缓存的操作,它是Spring为开发者定义的一套用于管理缓存的接口及相关实现。而Spring缓存抽象底层的概念与这五大接口的描述都是通用的,因此了解JSR-107定义的相关概念以及API接口描述,将有助于我们学习Spring的缓存抽象。

1.1 接口定义

1、CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。

2、CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。

3、Cache:这是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。

4、Entry:它是一个存储在Cache中的Key-Value对。

5、Expiry:每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新、删除。缓存有效期可以通过ExpiryPolicy设置。

1.2 接口关系图谱

二、Spring缓存抽象(以下重点)

2.1 Spring缓存接口

Spring从3.1开始定义了:org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们的开发。那么按照JSR-107的缓存思想,CacheManager就是用于管理Cache的,而Cache则是真正对缓存进行操作的抽象。

1、Cache接口是具体的缓存组件的规范定义,包含对缓存数据的各种操作;

2、Cache接口下Spring提供了各种xxxCache组件(实现类),如RedisCache,EhCacheCache,ConcurrentMapCache等。

每次调用需要缓存功能的方法时,Spring都会检查指定参数的指定目标方法是否已经被调用过。如果有就直接从缓存中获取方法调用后的结果;如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要注意以下两点:

1、确定哪些方法需要被缓存以及它们的缓存策略。

2、从缓存中读取之前缓存存储的数据。

2.2 Spring 缓存注解

最常用的缓存注解有如下:

@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。

@CacheEvict清空缓存。用于标注在一些删除方法上。

@CachePut保证方法被调用,又希望结果被缓存。用于标注在一些更新方法上,更新缓存。

@EnableCaching开启基于注解的缓存。

2.3 缓存策略

keyGengerator缓存数据时key的生成策略。

serialize缓存数据时value的序列化策略。

三、Spring缓存快速入门

自进入Spring Boot时代,很多功能都已经不再需要繁杂的Java代码来实现,而是使用注解来完成相同的功能,下面将介绍如何使用注解的方式来完成一整套关于Spring默认缓存数据的操作。

3.1 开启基于注解的缓存功能

首先,如果希望使用注解的方式使用缓存,那么就需要开启基于注解的缓存功能。

具体方法是在Spring Boot的主程序上加上@EnableCaching注解:

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

3.2 自定义缓存组件

CacheManager管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字。因此,在使用注解定义缓存组件的时候需要指定一些属性:

cacheNames / value:指定缓存组件的名称,两个属性都是指定缓存名称,二选一。

key:缓存数据使用的key。默认是使用方法参数的值。而缓存的数据就是方法返回值。另外key也可以使用SpEL表达式来表示。

keyGenerator:key的生成器,与key属性二选一。

cacheManager:指定缓存管理器,或者cacheResolver指定缓存解析器。

condition:指定符合条件的情况下才缓存。

unless:否定缓存。当unless指定的条件为true,方法返回值就不会缓存。也可以获取到结果判断是否需要缓存,如:        unless = “#result == null”,就代表如果结果是null就不缓存。

sync:是否使用异步模式进行缓存。注意!!sync属性默认为false,如果为true,则unless属性将不再支持

附 SpEL表达式指定key的规则 

3.3 代码示例

在查询全部课程的serviceImpl方法上使用缓存注解@Cacheable。

@Cacheable(cacheNames = "courses")  
public List<Course> findAllCourses() {  
    List<Course> allCourses = couseRep.findAll();  
    logger.info("查询全部课程 : " + allCourses);  
    return allCourses;
}

查询效果 : 

第一次查询,有SQL日志打印;第二次以后都没有SQL打印,说明缓存生效了

问题描述 

1、第一次在Service接口中标记了缓存注解,没有生效。原因很可能是因为当service组件自动注入的时候实则是实现类在真正执行操作,因此,只有在真正使用的组件上使用缓存才能够起作用。因此,以interface--Impl的形式开发的时候,要将缓存注解标记在具体实现类上,否则会失效。

2、给key属性赋值一个普通的字符串,报:SpelEvaluationException异常。因此这个key只能使用SpEl表达式来描述。像上面这种没有参数的情况,可以不必指定key属性,spring会默认为缓存数据生成一个key。

四、@Cacheable缓存工作原理

4.1 缓存组件的自动配置

缓存的相关配置来自于CacheAutoConfiguration类。

这个类使用@Import注解向容器中导入一个CacheConfigurationImportSelector的静态内部类,和其他自动配置时的导入选择器类似,它也是ImportSelector的实现类,这些实现类只有一个方法:String[] selectImports(...),专门用来导入具体的JavaConfig配置类。

而CacheConfigurationImportSelector,负责导入各种缓存组件的配置类。通过在selectImports内部打上断点debug启动项目的方式,我们可以一览返回值String[]中的内容:

[  
    org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration,   
    org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration  
]

这些缓存配置,就是Spring缓存抽象的具体底层实现所需要用到的JavaConfig。

这些配置类内部都会有一些规则判断,@ConditionalXxx来判断是否生效。以第一个GenericCacheConfiguration为例:

@Configuration
@ConditionalOnBean(Cache.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class GenericCacheConfiguration {

	private final CacheManagerCustomizers customizers;

	GenericCacheConfiguration(CacheManagerCustomizers customizers) {
		this.customizers = customizers;
	}

	@Bean
	public SimpleCacheManager cacheManager(Collection<Cache> caches) {
		SimpleCacheManager cacheManager = new SimpleCacheManager();
		cacheManager.setCaches(caches);
		return this.customizers.customize(cacheManager);
	}
}

可以看到类头上,相关的@Conditional注解来表明这个配置类在哪种情况下生效。但是我们也可以使用spring boot的自动配置报告来查看究竟是哪个缓存配置类生效。

4.2 SimpleCacheConfiguration

通过在全局配置文件中设置debug=true,使用spring boot的自动配置报告功能,打印匹配的JavaConfig配置类,我们可以看到默认启用的缓存配置是SimpleCacheConfiguration

打开这个配置类:

/**
 * Simplest cache configuration, usually used as a fallback.
 *
 * @author Stephane Nicoll
 * @since 1.3.0
 */
@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);
	}
}

这个配置类通过@Bean向容器中注册了一个ConcurrentMapCacheManager对象,这个CacheManager可以获取和创建ConcurrentMapCache类型的缓存组件。

4.3 缓存执行流程(重点)

使用@Cacheable时:

1、在方法service方法第一次执行前,先去查询Cache(缓存组件)。它是按照cacheNames指定的名字调用CacheManager中的getCache(String name)方法获取的。

@Override
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;
}

当第一次执行时未找到指定缓存,那么就会去调用createConcurrentMapCache(name)创建这个缓存,并将key-value放入到缓存中。

2、去Cache中查找缓存,使用一个key,这个key默认是使用SimpleKeyGenerator生成key。

SimpleKeyGengerator生成key的默认策略:

/**
 * Generate a key based on the specified parameters.
 */
public static Object generateKey(Object... params) {
	if (params.length == 0) {
		return SimpleKey.EMPTY;
	}
	if (params.length == 1) {
		Object param = params[0];
		if (param != null && !param.getClass().isArray()) {
			return param;
		}
	}
	return new SimpleKey(params);
}

如果没有参数,那么key就是用一个SimpleKey对象;如果有一个参数,则key = 参数的值;

如果有多个参数,key = new SimpleKey(params);

3、没有查找到缓存就调用目标方法。

4、将目标方法返回的结果,放入缓存中。

总结 

由@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询,如果没有就运行方法,并将结果放入缓存,以后再调用就可以直接是用缓存中的数据。

核心

1、是用CacheManager(默认ConcurrentMapCacheManager)按照名字得到Cache(默认ConcurrentMapCache)组件。

2、key使用keyGenerator生成的,默认的是SimpleKeyGenerator

五、自定义keyGenerator

自定义的key有两种实现手段,一种是通过@Cacheable的key属性,另一个是通过keyGenerator属性。

key属性使用的是SpEL表达式来指定key的格式规则,如:key = “#root.methodName+’[’+#id+’]’”,但是一种比较零散的自定义策略。

而keyGenerator可以指定一个通用的缓存key的生成策略。在这里简单说一下代码实现。

@Configuration
public class SysCacheKeyGenerator {

    /**
     * 课程缓存key生成策略
     */
    @Bean("courseDefaultKeyGenerator")
    public KeyGenerator courseCacheKeyGenerator() {
        return new KeyGenerator() {

            @Override
            public Object generate(Object target, Method method, Object... params) {
                String key = params.length == 0 ? "courseList" : params.toString();
                return method.getName() + "." + key;
            }
        };
    }
}

使用这段代码来定义一个自定义的keyGenerator,并注册到容器中,然后配置@Cacheable的keyGenerator属性:

@Override
@Cacheable(cacheNames = "courses", keyGenerator = "courseDefaultKeyGenerator")
public List<Course> findAllCourses() {
	List<Course> allCourses = couseRep.findAll();
	logger.info("查询全部课程 : " + allCourses);
	return allCourses;
}

那么实际缓存时就会使用我们自定义的key:

六、设置缓存条件

@Cacheable注解,

condition属性可以动态的判断数据是否满足缓存条件。

如:condition = “#id > 1”,意思是判断参数id的值大于1的情况下才对结果缓存。

那么它的用法主要依赖于SpEl表达式的逻辑判断。

unless属性的意思是“不缓存”,同样是通过SpEl表达式来判断true或false,如果unless的条件成立,那么将不会对数据进行缓存。

如:unless = “#id == 2” ,意思是如果id的值为2,那么就不缓存结果了。

七、@CachePut

@CachePut注解意为更新缓存。

一般标注在涉及到数据库更新操作的方法上,在更新数据库中记录的同时也会更新缓存中对应的数据。

运行实际

1、先调用目标方法

2、将目标方法的结果缓存起来。

注意 !!!

在使用@CachePut的时候要注意缓存数据的key要与查询该缓存数据时所用的key保持一致,否则,两个key如果不一致,就会出现查询方法依然是缓存更新之前的数据。

这里key可以使用#result来取得返回结果,及其内部属性如#result.id,但是@Cacheable不能使用#result来取得结果值,想想这是为什么?(提示,执行时机)

八、@CacheEvict

@CacheEvict :清除缓存。

key:指定要清除的缓存。

allEntries = true 代表清除这个缓存中的所有数据。

beforeInvocation = false 代表清除缓存的操作是在方法执行后,这也是默认值。如果出现异常,那么就不会清除缓存。该属性如果为true,那么就代表在方法执行前清除缓存,无论方法是否出现异常。

九、@Caching与@CacheConfig

@Caching是一个复杂缓存的注解,可以指定多种缓存规则;@CacheConfig放在类头上,用于抽取一些公共的缓存属性配置,如可以指定公共的缓存名称。

总结

关于Spring默认缓存组件的使用,基本就是@Cacheable、@CachePut、@CacheEvict这三个注解,另外注意在使用它们之前,记得在Spring Boot的程序主类上开启@EnableCaching。

另外,一定要注意@Cacheable和@CachePut的执行时机,以及它们真正操作的key值,因为这个缓存数据是存储在内存中的,因此不像数据库中的数据那样直观明了,操作缓存中的数据要求开发者有比较好的数据管理规则,否则,很容易出现缓存数据失效的问题。

综上,就是关于Spring Boot使用默认缓存管理器的缓存数据的基本操作方法和基本工作原理,欢迎文末留言。

猜你喜欢

转载自blog.csdn.net/u014745069/article/details/86438487