目录
6 @CachePut&@CacheEvict&@CacheConfig
目录
6 @CachePut&@CacheEvict&@CacheConfig
1 JSR107
JSR是Java Specification Requests 的缩写 ,Java规范请求,故名思议提交Java规范, JSR-107呢就是关于如何使用缓存的规范,是java提供的一个接口规范,类似于JDBC规范,没有具体的实现,具体的实现就是reids等这些缓存。
1.1 JSR107核心接口
Java Caching(JSR-107)定义了5个核心接口,分别是CachingProvider、CacheManager、
Cache、Entry和Expiry。
CachingProvider(缓存提供者):创建、配置、获取、管理和控制多个CacheManager
CacheManager(缓存管理器):创建、配置、获取、管理和控制多个唯一命名的Cache,
Cache存在于CacheManager的上下文中。一个CacheManager仅对应一个CachingProvider
Cache(缓存):是由CacheManager管理的,CacheManager管理Cache的生命周期,
Cache存在于CacheManager的上下文中,是一个类似map的数据结构,并临时存储以key为索引的值。一个Cache仅被一个CacheManager所拥有
Entry(缓存键值对):是一个存储在Cache中的key-value对
Expiry(缓存时效):每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个时间,条目就自动过期,过期后,条目将不可以访问、更新和删除操作。缓存有效期可以通过ExpiryPolicy设置
1.2 JSR107图示
一个应用里面可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获取到多个缓存管理器(CacheManager),一个缓存管理器管理着不同的缓存(Cache),缓存中是一个个的缓存键值
对(Entry),每个entry都有一个有效期(Expiry)。缓存管理器和缓存之间的关系有点类似于数据库中连接池和连接的关系。
使用JSR-107需导入的依赖
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
2 Spring的缓存抽象
2.1 缓存抽象定义
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用Java
Caching(JSR-107)注解简化我们进行缓存开发。
Spring Cache 只负责维护抽象层,具体的实现由自己的技术选型来决定。将缓存处理和缓存技术解除耦合。
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
- 确定那些方法需要被缓存
- 缓存策略
2.2 重要接口
Cache:缓存抽象的规范接口,缓存实现有:RedisCache、EhCache、
ConcurrentMapCache等
CacheManager:缓存管理器,管理Cache的生命周期
3 Spring缓存使用
3.1 重要概念&缓存注解
案例实践之前,先介绍下Spring提供的重要缓存注解及几个重要概念
几个重要概念&缓存注解:
概念/注解 | 作用 |
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、 ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
说明:
- @Cacheable标注在方法上,表示该方法的结果需要被缓存起来,缓存的键由keyGenerator的策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之后,在缓存时效内再次调用该方法时将不会调用方法本身而是直接从缓存获取结果
- @CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新
3.2 环境搭建
- 创建SpringBoot应用:选中Mysql、Mybatis、Web模块
- 创建数据库表
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NxQULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
(3)创建表对应的实体Bean
@Data
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; //性别 1男 0女
private Integer dId;
}
@Data
public class Department {
private Integer id;
private String departmentName;
}
(4)整合mybatis操作数据库 数据源配置:驱动可以不写,SpringBoot会根据连接自动判断
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_h
spring.datasource.username=root
spring.datasource.password=root
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#开启驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
使用注解版Mybatis:使用@MapperScan指定mapper接口所在的包
@SpringBootApplication
@MapperScan(basePackages = "com.lagou.cache.mappers")
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
创建对应的mapper接口
public interface EmployeeMapper {
@Select("SELECT * FROM employee WHERE id = #{id}")
public Employee getEmpById(Integer id);
@Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#
{lastName},#{email},#{gender},#{dId})")
public void insertEmp(Employee employee);
@Update("UPDATE employee SET lastName = #{lastName},email = #
{email},gender = #{gender},d_id = #{dId} WHERE id = #{id}")
public void updateEmp(Employee employee);
@Delete("DELETE FROM employee WHERE id = #{id}")
public void deleteEmpById(Integer id);
}
编写Service:
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmpById(Integer id){
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
}
编写Controller:
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmp(@PathVariable("id") Integer id){
return employeeService.getEmpById(id);
}
}
测试: 测试之前可以先配置一下Logger日志,让控制台将Sql打印出来:
logging.level.com.lagou.cache.mappers=debug
结论:当前还没有看到缓存效果,因为还没有进行缓存的相关设置
3.3 @Cacheable初体验
① 开启基于注解的缓存功能:主启动类标注@EnableCaching
@SpringBootApplication @MapperScan(basePackages = "com.lagou.cache.mappers") @EnableCaching //开启基于注解的缓存 public class SpringbootCacheApplication { public static void main(String[] args) { SpringApplication.run(SpringbootCacheApplication.class, args); }
② 标注缓存相关注解:@Cacheable、CacheEvict、CachePut @Cacheable:将方法运行的结果进行缓存,以后再获取相同的数据时,直接从缓存中获取,不再 调用方法
@Cacheable(cacheNames = {"emp"})
public Employee getEmpById(Integer id){
Employee emp = employeeMapper.getEmpById(id); return emp;
}
@Cacheable注解的属性:
属性名 | 描述 |
cacheNames/value | 指定缓存的名字,缓存使用CacheManager管理多个缓存组件 Cache,这些Cache组件就是根据这个名字进行区分的。对缓存的真正CRUD操作在Cache中定义,每个缓存组件Cache都有自己唯一的名字,通过cacheNames或者value属性指定,相当于是将缓存的键值对进行分组,缓存的名字是一个数组,也就是说可以将一个缓存键值对分到多个组里面 |
key | 缓存数据时的key的值,默认是使用方法参数的值,可以使用SpEL表达式计算key的值 |
keyGenerator | 缓存的生成策略,和key二选一,都是生成键的,keyGenerator可自定义 |
cacheManager | 指定缓存管理器(如ConcurrentHashMap、Redis等) |
cacheResolver | 和cacheManager功能一样,和cacheManager二选一 |
condition | 指定缓存的条件(满足什么条件时才缓存),可用SpEL表达式(如 #id>0,表示当入参id大于0时才缓存) |
unless | 否定缓存,即满足unless指定的条件时,方法的结果不进行缓存, 使用unless时可以在调用的方法获取到结果之后再进行判断(如 #result==null,表示如果结果为null时不缓存) |
sync | 是否使用异步模式进行缓存 |
注:
①既满足condition又满足unless条件的也不进行缓存 ②使用异步模式进行缓存时(sync=true):unless条件将不被支持 可用的SpEL表达式见下表:
名字 | 位置 | 描述 | 示例 |
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代表参数的索引 | #iban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效, 如"unless","cache put"的表达式,"cache evict"的表达式 beforeInvocation=false) | #result |
4 缓存自动配置原理源码剖析
在springBoot中所有的自动配置都是 ...AutoConfiguration 所以我们去搜 CacheAutoConfiguration 这个类 在这个类中有一个静态内部类 CacheConfigurationImportSelector 他有一个 selectImport 方 法是用来给容器中添加一些缓存要用的组件
我们在这里打上断点,debug调试一下看看 imports 中有哪些缓存组件
我们在这里打上断点,debug调试一下看看 imports 中有哪些缓存组件
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class) // classpath下要存在对应的
class文件才会进行配置
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
十个缓存组件:最终会发现只有 SimpleCacheConfiguration 是被使用的,所以也就说明默认情况 下使用 SimpleCacheConfiguration ; 然后我们进入到 SimpleCacheConfiguration 中:
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}
}
我们会发现他给springBoot容器添加了一个bean,是一个 CacheManager ; ConcurrentMapCacheManager 实现了 CacheManager 接口 再来看 ConcurrentMapCacheManager 的getCache方法
@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;
}
getCache 方法使用了双重锁校验(这种验证机制一般是用在单例模式中) 我们可以看到如果没有 Cache 会调用 cache = this.createConcurrentMapCache(name);
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ?this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);
}
这个方法会创建一个 ConcurrentMapCache 这个就是我们说的 Cache ;
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
private final ConcurrentMap<Object, Object> store;
@Nullable
private final SerializationDelegate serialization;
在这个类里面有这样三个属性;
private final ConcurrentMap store;
这个就是前文中的 Entry 用来存放键值对; 在 ConcurrentMapCache 中我们会看到一些操作 Cache 的方法,选几个重要的
@Override
@Nullable
protected Object lookup(Object key) {
return this.store.get(key);
}
lookup 方法是根据key来找value的
@Override
public void put(Object key, @Nullable Object value) {
this.store.put(key, toStoreValue(value));
}
put 方法顾名思义是用来添加键值对的; 到这里基本上就结束了,接下来我们来详细分析一下 @Cacheable 注解
5 @Cacheable源码分析
我们在上述的两个方法上打上断点;debug运行springBoot; 访问getEmp接口
我们会发现他来到了lookup方法这里,说明注解的执行在被注解的方法前,然后这里我们会返回 null;
我们放行到下一个注解会发现;调用了put方法
添加了Cache; 然后我们第二次对getEmp接口发起请求我们会发现这一次缓存内容不再为null
@Cacheable运行流程:
①方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取(CacheManager 先获取相应的缓存,第一次获取缓存如果没有Cache组件会自动创建)
②去Cache中查找缓存的内容,使用的key默认就是方法的参数:
key默认是使用keyGenerator生成的,默认使用的是SimpleKeyGenerator
SimpleKeyGenerator生成key的默认策略:
如果没有参数:key = new SimpleKey();
如果有一个参数:key = 参数的值
如果有多个参数:key = new SimpleKey(params);
③没有查到缓存就调用目标方法
④将目标方法返回的结果放进缓存中
总结:@Cacheable标注的方法在执行之前会先检查缓存中有没有这个数据,默认按照参数的值为key查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用时直接使用缓存中的数据。
核心:
- 使用CacheManager(ConcurrentMapCacheManager)按照名字得到
Cache(ConcurrentMapCache)组件
- key使用keyGenerator生成,默认使用SimpleKeyGenerator
6 @CachePut&@CacheEvict&@CacheConfig
@CachePut
- 说明:既调用方法,又更新缓存数据,一般用于更新操作,在更新缓存时一定要和想更新的缓存有相同的缓存名称和相同的key(可类比同一张表的同一条数据)
- 运行时机:
3、示例:
@CachePut(value = "emp",key = "#employee.id")
public Employee updateEmp(Employee employee){
employeeMapper.updateEmp(employee);
return employee;
}
总结 :@CachePut标注的方法总会被调用,且调用之后才将结果放入缓存,因此可以使用#result 获取到方法的返回值。
@CacheEvict
- 说明:缓存清除,清除缓存时要指明缓存的名字和key,相当于告诉数据库要删除哪个表中的哪条数据,key默认为参数的值
- 属性:
value/cacheNames:缓存的名字
key:缓存的键
allEntries:是否清除指定缓存中的所有键值对,默认为false,设置为true时会清除缓存中的所有键值对,与key属性二选一使用 beforeInvocation:在@CacheEvict注解的方法调用之前清除指定缓存,默认为false,即在方法调用之后清除缓存,设置为true时则会在方法调用之前清除缓存(在方法调用之前还是之后清除缓存的区别在于方法调用时是否会出现异常,若不出现异常,这两种设置没有区别,若出现异常,设置为在方法调用之后清除缓存将不起作用,因为方法调用失败了)
示例:
@CacheEvict(value = "emp",key = "#id",beforeInvocation = true)
public void delEmp(Integer id){
employeeMapper.deleteEmpById(id);
}
@CacheConfig
- 作用:标注在类上,抽取缓存相关注解的公共配置,可抽取的公共配置有缓存名字、主键生成器等(如注解中的属性所示)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
2、示例:通过@CacheConfig的cacheNames 属性指定缓存的名字之后,该类中的其他缓存注 解就不必再写value或者cacheName了,会使用该名字作为value或cacheName的值,当然也遵循 就近原则
@Service
@CacheConfig(cacheNames = "emp")
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
@Cacheable
public Employee getEmpById(Integer id) {
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
@CachePut(key = "#employee.id")
public Employee updateEmp(Employee employee) {
employeeMapper.updateEmp(employee);
return employee;
}
@CacheEvict(key = "#id", beforeInvocation = true)
public void delEmp(Integer id) {
employeeMapper.deleteEmpById(id);
}
}
7 基于Redis的缓存实现
SpringBoot默认开启的缓存管理器是ConcurrentMapCacheManager,创建缓存组件是 ConcurrentMapCache,将缓存数据保存在一个个的ConcurrentHashMap<Object, Object>中。
开发时我们可以使用缓存中间件:redis、memcache、ehcache等,这些缓存中间件的启用很简单——只要向容器中加入相关的bean就会启用,可以启用多个缓存中间件
7.1 安装启动Redis
7.2 整合Redis
①引入Redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入redis的starter之后,会在容器中加入redis相关的一些bean,其中有两个跟操作redis相关的:
RedisTemplate和StringRedisTemplate(用来操作字符串:key和value都是字符串),template中封装了操作各种数据类型的操作(stringRredisTemplate.opsForValue()、stringRredisTemplate.opsForList()等)
@Configuration
@ConditionalOnClass({JedisConnection.class, RedisOperations.class,Jedis.class})
@EnableConfigurationProperties({RedisProperties.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Configuration
protected static class RedisConfiguration {
protected RedisConfiguration() {
}
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"})
public RedisTemplate<Object, Object>redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean({StringRedisTemplate.class})
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws
UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
//...
②配置redis:只需要配置redis的主机地址(端口默认即为6379,因此可以不指定)
spring.redis.host=127.0.0.1
③测试:
使用redis存储对象时,该对象必须可序列化(实现Serializable接口),否则会报错,此时存储的结果在redis的管理工具中查看如下:由于序列化的原因值和键都变为了另外一种形式
SpringBoot默认采用的是JDK的对象序列化方式,
我们可以切换为使用JSON格式进行对象的序列化操作,这时需要我们自定义序列化规则(当然我们也可以使用Json工具先将对象转化为Json格式之后再保存至redis,这样就无需自定义序列化)
8 自定义RedisCacheManager
8.1 Redis注解默认序列化机制
打开Spring Boot整合Redis组件提供的缓存自动配置类
RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该
类的源码信息,其核心代码如下
@Configuration
class RedisCacheConfiguration {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader()))
;
List<String> cacheNames = this.cacheProperties.getCacheNames();
if(!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader){
if(this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration
config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
...
return config;
}
}
}
从上述核心源码中可以看出,RedisCacheConfiguration内部同样通过Redis连接工厂 RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制 RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。 如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代 码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可
8.2 自定义RedisCacheManager
在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为 cacheManager的Bean组件
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory
redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)).disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的 cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和 value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String 格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用 entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天 完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果 进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接 口)