1.JSR107
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设置。
2.Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们开发。
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。
- Cache接口下Spring提供了各种xxxCache的实现:如RedisCache,EhCacheCache,ConcurrentMapCache等。
- 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过,如果被调用过,就直接从缓存中获取方法调用后的结果,如果没有就调用方法。并缓存结果后返回给用户,下次调用直接从缓存中获取。
- 使用Spring缓存抽象时我们需要关注以下两点:
- 确定哪些方法需要被缓存以及它们的缓存策略
- 从缓存中读取之前缓存存储的数据
3.几个重要概念&缓存注解
Cache | 缓存接口,用于定义缓存操作。实现类:RedisCache,EhCacheCache,ConcurrentMapCache等 |
CacheManager | 缓存管理器,用于管理各种缓存(Cache)组件 |
@Cacheable | 此注解添加到方法上,能够根据方法的请求参数对结果进行缓存,用于查询方法 |
@CacheEvict | 此注解添加到方法上,清空缓存,用于删除方法 |
@CachePut | 此注解添加到方法上,保证方法被调用,同时结果也会被缓存,用于更新方法 |
@EnableCaching | 此注解添加到类上,开启基于注解的缓存 |
keyGenerator | 缓存数据key的生成策略 |
serialize | 缓存数据时value的序列化策略 |
@Cacheable,@CachePut,@CacheEvict主要参数含义
value或cacheNames | 指定缓存组件的名称,理解为存储在哪个缓存库,支持多值,必须指定一个 | 例如:@Cacheable(value="myCache") |
value | 缓存的key,可以为空 如果为空,按照KeyGenerator进行生成,默认使用SimpleKeyGenerator.generateKey()来生成 如果指定了KeyGenerator,就按照自定义的KeyGenerator来生成 另外,还可以自己指定值,需要按照SpEL表达式的方式,或者使用KeyGenerator来生成 | 方法名+参数的形式可以避免key重复,例如:@Cacheable(key="#root.methodName + '[' + #id + ']'") |
condition | 缓存的条件,可以为空,使用SpEL表达式书写,返回true或false,只有为true的时候,才会添加/删除缓存 | 例如:@Cacheable(condition="#userName.length()>2") |
allEntries(@CacheEvict) | 是否清空所有缓存,缺省为false,如果指定为true,则方法调用后立刻清空所有缓存 | 例如:@CacheEvict(allEntries=true) |
beforeInvocation(@CacheEvict) | 是否在方法执行前清除缓存,缺省为false,如果指定为true,则方法执行前清除缓存。在方法执行前清除缓存的作用:如果方法执行抛出异常,照样可以清除缓存 | 例如:@CacheEvict(beforeInvocation=true) |
unless(@CachePut,@Cacheable) | 用于否定缓存的,表达式只在方法执行后判断,因此可以拿到返回值进行判断,如果条件为true,则不缓存,条件为fasle,进行缓存 | 例如:@Cacheable(unless="result == null") |
Cache SpEL available metadata
名字 | 位置 | 描述 | 示例 |
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
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,cachePut的表达式,cacheEvict的表达式beforeInvocation=false) | #result |
4.缓存的使用
1.搭建基本环境
新建Spring Boot项目,添加Spring Web、Mybatis Framework、MySQL Driver、Spring cache abstraction依赖。
新建数据库cache,导入SQL文件,添加两张表。在com.atguigu.cache.bean包下,添加对应的两个Bean。
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for employee
-- ----------------------------
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;
配置数据库连接信息。
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/cache?useSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.atguigu.cache.mapper: debug
在com.atguigu.cache.mapper下添加mapper文件,在mapper上添加@Mapper注解,或者在主启动类上加上@MapperScan注解,@Mapper和@MapperScan有一个即可,用于保证能扫描到这个Mapper文件。然后使用注解的方式,在Mapper文件里添加对应的CRUD方法。随后,在测试类中测试效果,测试成功表明Mybatis配置成功。
在com.atguigu.cache.controller下添加Controller类,在com.atguigu.cache.service下添加Service类,编写相应代码,通过浏览器访问http://localhost:8080/emp/1,可以在浏览器看到返回的json数据。
package com.atguigu.cache.controller;
import com.atguigu.cache.bean.Employee;
import com.atguigu.cache.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping("/emp/{id}")
public Employee getEmployeeById(@PathVariable("id") int id) {
return employeeService.getEmployeeById(id);
}
}
package com.atguigu.cache.service;
import com.atguigu.cache.bean.Employee;
import com.atguigu.cache.mapper.EmployeeMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmployeeService {
@Autowired
EmployeeMapper employeeMapper;
public Employee getEmployeeById(int id) {
System.out.println("查询" + id + "号员工");
Employee employee = employeeMapper.getEmployeeById(id);
return employee;
}
}
package com.atguigu.cache.mapper;
import com.atguigu.cache.bean.Employee;
import org.apache.ibatis.annotations.*;
@Mapper
public interface EmployeeMapper {
@Select("SELECT * FROM employee WHERE id = #{id}")
public Employee getEmployeeById(int id);
@Update("UPDATE employee SET lastName = #{lastName}, email = #{email}, gender = #{gender}, d_id = #{dId} WHERE id = #{id}")
public void updateEmployee(Employee employee);
@Delete("DELETE FROM employee WHERE id = #{id}")
public void deleteEmplyeeById(int id);
@Insert(("INSET INTO employee(lastName, email, gender, d_id) VALUES (#{lastName}, #{email}, #{gender}, #{dId})"))
public void insertEmployee(Employee employee);
}
2.缓存的用法
在主启动类上加上@EnableCaching注解,用于开启基于注解的缓存。给对应的service方法上添加@Cacheable或@CacheEvict或@CachePut注解,进行测试。
@Cacheable注解
@Cacheable 将方法的运行结果进行缓存,以后再获取相同数据的时候,直接从缓存中获取,不需要再调用方法了
@Cacheable注解的几个属性:
- cacheNames/value:指定缓存组件的名字,可以理解成缓存在哪个缓存库里面,可以指定多个缓存库,用大括号括起来,英文逗号分隔
- key:缓存数据使用的key,默认使用方法的参数值,也可以编写SpEl表达式
- keyGenerator:指定key的生成器,key和keyGenerator二选一即可
- cacheManager:指定缓存管理器,它的getCache()方法返回一个cache
- cacheResolver:指定缓存解析器,它的resolveCaches()方法返回一个cache的集合
- cacheManager和cacheResolver制定一个即可
- condition:指定缓存条件,当condition为true的时候,进行缓存
- unless:指定不缓存条件,当unless为true的时候,不进行缓存
- syne:缓存是否使用异步模式
在getEmployee()的service方法上,添加注解@Cacheable(cacheNames="employee"),重启进行测试,第一次访问http://localhost:8080/emp/1,在控制台可以看到SQL信息,第二次访问,就不会打印SQL信息了,表明第二次访问走的缓存。
自定义KeyGenerator
编写一个MyCacheConfig的类,可以在里面自定义KeyGenerator。
package com.atguigu.cache.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
@Configuration
public class MyCacheConfig {
// 指定KeyGenerator的名称,用于在@Cacheable中,使用KeyGenerator属性进行指定
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
return method.getName() + Arrays.asList(objects);
}
};
}
}
@CachePut
同理,编写controller方法和service方法做更新操作。
@CachePut 既调用方法,又更新缓存数据
应用场景:修改数据库的某条数据,同时更新缓存
@CachePut的调用时机和@Cacheable不同,@Cacheable在方法执行前调用,@CachePut在方法执行完后再调用
@CachePut先调用目标方法,再将目标方法的结果缓存起来。
// Controller
@GetMapping("/emp")
public Employee updateEmployee(Employee employee) {
return employeeService.updateEmployee(employee);
}
// Service
// @CachePut没有带key属性,后面更新时候,更新的key是employee对象对应的缓存对象,和查询时候的缓存对象的key不一致
@CachePut(cacheNames = "employee")
public Employee updateEmployee(Employee employee) {
System.out.println("更新员工信息:" + employee);
employeeMapper.updateEmployee(employee);
return employee;
}
做测试,访问http://localhost:8080/emp/1,第一次在控制台可以看到SQL输出,第二次访问,正常走缓存,发送更新请求:http://localhost:8080/emp?id=1&lastName=lisi&email=lisi@gmail.com&gender=1&dId=2,执行更新请求,再次访问http://localhost:8080/emp/1,却发现并没有拿到更新后的结果,这是什么问题呢?
此时回看@Cacheable方法上的注解,因为前面使用了@KeyGenerator自定义key,所以,查询时候,向缓存map中放缓存的时候,使用的是key是getEmployeeById[1],再看更新方法的@CachePut注解,没有使用Key或KeyGenerator,于是,更新缓存时候,用的key是Employee对象,于是,更新的缓存并不是原来key=getEmployeeById[1]的缓存,所以第二次请求http://localhost:8080/emp/1的时候,继续拿key=getEmployeeById[1]的缓存,也就是旧的内容了,因为这个地方,对于同一个缓存对象,它的key要是一致的。
简单起见,这里将@Cacheable的注解改成@Cacheable(cacheNames = "employee", key = "#id"),将@CachePut的注解改成@CachePut(cacheNames = "employee", key = "#employee.id"),此时对于同一个缓存对象,它的key都是id值,再次按照上面的测试方法,更新缓存后,再次获取的结果就是更新后的结果了。
@CacheEvict
用于清除缓存。编写controller和service方法。因为上面更新时候,出过问题,所以这里的id一定要注意,保证同一缓存对象,前后的id是一样的,才能操作成功。
// Controller
@DeleteMapping("/deleteEmp")
public void deleteEmployeeById(int id) {
employeeService.deleteEmployeeById(id);
}
// Service
@CacheEvict(cacheNames = "employee", key = "#id")
public void deleteEmployeeById(int id) {
System.out.println("删除员工:" + id);
// employeeMapper.deleteEmplyeeById(id);
}
首先访问一下1号员工http://localhost:8080/emp/1,通过控制台可知,执行了目标方法,并将结果进行了缓存,再发送http://localhost:8080/deleteEmp?id=1请求,将1号员工的缓存信息删除,再访问1号员工http://localhost:8080/emp/1,通过控制台,可以发现,又一次执行了目标方法,说明前面的一次清除缓存是成功的。
@CacheEvict里,还有两个属性:
- allEntries,默认是false,当allEntries=true的时候,表示删除全部缓存。
- beforeInvocation,默认是false,当beforeInvocation=true,表示缓存的清除在方法之前执行。
@Caching
@Caching里有cachable()、put()、evict()三个方法,可以自定义cache的规则。
编写controller,service,mapper,重启服务,请求http://localhost:8080/emp/lastName/lisi,第一次会调用目标方法,并将key=lastName,key=result.id,key=result.email,value=employee对象放到缓存中,我们再次请求http://localhost:8080/emp/1,可以发现,是没有走缓存的,说明此时@Caching是生效的,同时以key=result.id添加了缓存。
// Controller
@GetMapping("/emp/lastName/{lastName}")
public Employee getEmployeeByLastName(@PathVariable("lastName") String lastName) {
return employeeService.getEmployeeByLastName(lastName);
}
// Service
// 在执行方法的时候,根据key=#lastName查询,如果查询不到,添加key=#lastName,value=Employee对象的缓存
// 同时,还会把key=#result.id和key=#result.email,value=Employee的缓存对象,也放进缓存中
@Caching(
cacheable = {@Cacheable(cacheNames = "employee", key = "#lastName")},
put = {
@CachePut(cacheNames = "employee", key = "#result.id"),
@CachePut(cacheNames = "employee", key = "#result.email")
}
)
public Employee getEmployeeByLastName(String lastName) {
return employeeMapper.getEmployeeByLastName(lastName);
}
// Mapper
@Select("SELECT * FROM employee WHERE lastName = #{lastName}")
Employee getEmployeeByLastName(String lastName);
@CacheConfig
@CacheConfig注解作用于类上,使用它来做一个类内的全局的配置,可以配置cacheNames、keyGenerator、cacheManager、cacheResolver,那么下面的方法上的注解属性就可以去掉了。
3.原理:
找到CacheAutoConfiguration类,它上面有一个@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})注解,我们打开CacheConfigurationImportSelector类,在结尾处打一个断点,启动之后,可以看到,这里导入了10个配置类。
- 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.SimpleCacheConfiguration
- org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
在yml里加上debug: true后,重启项目,在控制台可以看到,默认情况下,是SimpleCacheConfiguration匹配,也就是只有它的配置是生效的。找到SimpleCacheConfiguration类,可以看到,它向容器中注册了一个ConcurrentMapCacheManager的Bean。打开ConcurrentMapCacheManager类,可以看到有一个cacheMap,这个就是用来存储缓存的。
流程:
- 在Service方法运行之前,先按照注解指定的名称cacheName查找缓存,对应在ConcurrentMapCacheManager中的getCache()方法。第一次访问的时候,cache是null,后面会调用一个createConcurrentMapCache()方法创建一个cacheMap。
- 调用ConcurrentMapCache.lookup()方法在cacheMap中查找缓存,使用一个key,默认是方法的参数,key是按照某种策略生成的。默认使用SimpleKeyGenerator来生成key,也就是SimpleKeyGenerator.generateKey()方法,方法里会根据参数的个数以及参数的类型生成key。
- 第一次调用的时候,lookup()为空,会调用Service方法,请求数据库,之后会执行ConcurrentMapCache.put()方法,将数据库查到的内容存入cacheMap中。第二次调用的时候,lookup()不为空,直接将缓存拿到的结果返回。
5.整合Redis实现缓存
1.搭建基本环境&测试Redis基本用法
从虚拟机上下载Redis镜像:docker pull docker.io/redis。
启动Redis镜像:docker run -d -p 6379:6379 redis。
使用Redis DeskTop Manager连接,便于查看里面的数据。下面在项目中进行引入Redis。
在pom.xml里加入redis的依赖,修改yml配置文件,加入spring:redis:host: 192.168.0.123。编写测试类。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis的五大数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)。
@Test
public void test01() {
// stringRedisTemplate.opsForValue();// 字符串
// stringRedisTemplate.opsForList();// 列表
// stringRedisTemplate.opsForSet();// 集合
// stringRedisTemplate.opsForHash();// 散列
// stringRedisTemplate.opsForZSet();// 有序集合
// 向Redis里保存数据
// stringRedisTemplate.opsForValue().append("msg", "hello");
// 从Redis里读取数据
// String msg = stringRedisTemplate.opsForValue().get("msg");
// System.out.println(msg);
// 向list中放数据
// stringRedisTemplate.opsForList().leftPush("myList", "1");
// stringRedisTemplate.opsForList().leftPush("myList", "2");
}
@Test
public void test02() {
// 先让Employee类实现Serializable接口,否则会报错
Employee employee = employeeMapper.getEmployeeById(1);
// redis保存对象,默认使用JDK的序列化机制,保存的序列化后的对象
redisTemplate.opsForValue().set("employee1", employee);
}
因为JDK的序列化器处理过后的信息不便于其他环境的转换,建议改成json数据格式进行存储。在config包下创建MyRedisConfig.java文件。
package com.atguigu.cache.config;
import com.atguigu.cache.bean.Employee;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.net.UnknownHostException;
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate<Object, Employee> EmployeeRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new Jackson2JsonRedisSerializer<Employee>(Employee.class));
return template;
}
}
在测试类中,注入添加了Serializer的RedisTemplate进行测试,通过Redis Desktop Manager查看结果,可以发现是json格式。
@Autowired
RedisTemplate<Object, Employee> employeeRedisTemplate;
@Test
public void test02() {
Employee employee = employeeMapper.getEmployeeById(1);
// redis保存对象,默认使用JDK的序列化机制,保存的序列化后的对象
// redisTemplate.opsForValue().set("employee1", employee);
employeeRedisTemplate.opsForValue().set("employee1", employee);
}
2.测试Redis缓存
原理:CacheManager缓存组件来实际给缓存中存取数据
- 引入Redis的starter后,容器中保存的是RedisCacheManager。
- RedisCacheManager帮我们创建RedisCache作为缓存组件,RedisCache是通过操作Redis缓存数据。
- 保存数据k-v都是Object时候,默认使用JDK的序列化。
- 自定义CacheManager,实现json序列化。
在MyRedisConfig.java里加上如下代码,实现对象的json序列化。
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
编写controller,service,mapper进行测试。
package com.atguigu.cache.controller;
import com.atguigu.cache.bean.Department;
import com.atguigu.cache.service.DepartmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DepartmentController {
@Autowired
DepartmentService departmentService;
@GetMapping("/department/{id}")
public Department getDepartmentById(@PathVariable("id") int id) {
return departmentService.getDepartmentById(id);
}
@GetMapping("save")
public void saveDepartment() {
departmentService.saveDepartment();
}
}
package com.atguigu.cache.service;
import com.atguigu.cache.bean.Department;
import com.atguigu.cache.mapper.DepartmentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class DepartmentService {
@Autowired
DepartmentMapper departmentMapper;
@Autowired
CacheManager cacheManager;
@Cacheable(cacheNames = "department")
public Department getDepartmentById(int id) {
System.out.println("查询部门" + id);
return departmentMapper.getDepartmentById(id);
}
public void saveDepartment() {
System.out.println("编码方式保存到Redis");
Cache cache = cacheManager.getCache("department");
cache.put("key", "value");
}
}
package com.atguigu.cache.mapper;
import com.atguigu.cache.bean.Department;
import org.apache.ibatis.annotations.*;
@Mapper
public interface DepartmentMapper {
@Select("SELECT * FROM department WHERE id = #{id}")
Department getDepartmentById(int id);
}