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设置.
- 如果要使用JSR107, 需要导入如下依赖
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <version>1.1.1</version> </dependency>
Spring缓存抽象
- 简介
- JSR107规范的技术复杂性较高, Spring3.1定义了Cache和CacheManager接口来统一不同的缓存技术.
- 当然, 支持使用JSR107注解来简化我们的开发.
-
Cache接口为缓存的组件规范定义, 包含缓存的各种操作集合.
- Cache接口下Spring提供了各种xxxCache的实现, 如RedisCache.
-
每次调用需要缓存功能的方法时, Spring会检查检查指定参数的指定的目标方法是否已经被调用过. 如果有就直接从缓存中获取方法调用后的结果, 如果没有就调用方法并缓存结果后返回给用户. 下次调用直接从缓存中获取.
- 使用Spring缓存抽象时需要关注两点
- 确定方法需要被缓存以及他们的缓存策略.
- 从缓存中读取之前缓存存储的数据.
- 几个重要概念
- Cache: 缓存接口, 定义缓存操作, 实现有RedisCache, EhCacheCache等.
- CacheManager: 缓存管理器, 管理各种Cache.
- @Cacheable: 主要针对方法配置, 能根据方法的请求参数对其结果进行缓存.
- @CacheEvict: 清空缓存, 如给删除方法上加该注解.
- @CachePut: 保证方法被调用, 又希望结果被缓存, 经常用于更新缓存.
- 和@Cacheable的区别
- @Cacheable是注解中有就不调用方法了.
- 而@CachePut无论如何都会调用方法, 然后把新结果存入缓存.
- 和@Cacheable的区别
- @EnableCaching: 开启基于注解的缓存.
- keyGenerator: 缓存数据时key生成策略.
- serialize: 缓存数据时value序列化策略.
- 搭建缓存的基本环境
- 引入spring-boot-starter-cache模块
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
- 主启动类
/** * 搭建基本环境 * 1. 创建数据库spring_cache, 并创建department和employee表 * 2. 创建javaBean封装数据 * 3. 整合Mybatis操作数据库 * 1. 配置数据源信息 * 2. 使用注解版Mybatis * 1) @MapperScan指定需要扫描的mapper接口所在的包 * 2) 对应的mapper接口要带@Mapper主机 * * 快速体验缓存 * 1. 开启基于注解的缓存. * 2. 标注缓存注解 */ @EnableCaching @MapperScan("top.binwenhome.cache.mapper") @SpringBootApplication public class SpringBootCache2Application { public static void main(String[] args) { SpringApplication.run(SpringBootCache2Application.class, args); } }
- 如EmployeeMapper
@Mapper public interface EmployeeMapper { @Select("SELECT * FROM employee WHERE id = #{id}") public Employee getEmpById(Integer id); }
- 搭建service, controller层
@Service public class EmployeeService { @Autowired private EmployeeMapper employeeMapper; public Employee getEmp(Integer id) { System.out.println("查询" + id + "号员工"); Employee emp = employeeMapper.getEmpById(id); return emp; } } @RestController public class EmployeeController { @Autowired private EmployeeService employeeService; @GetMapping("/emp/{id}") public Employee getEmployee(@PathVariable("id")Integer id) { Employee emp = employeeService.getEmp(id); return emp; } }
- 缓存中的SpEL用法
- @Cacheable注解
- 将方法的结果进行缓存, 以后再要相同的数据, 就不用从数据库查了.
- CacheManager: 管理多个Cache组件, 对缓存的真正CRUD操作在Cache组件中, 每个缓存组件都有一个唯一的名字.
- 几个属性
- cacheNames/value: 指定缓存的名字, 将方法的返回结果放在哪个缓存中, 是数组的方式.
- key/keyGenerator
- key: 缓存数据使用的key. 可以用它来指定. 默认是使用方法参数的值.
- keyGenerator: key的生成器: 可以自己指定key的生成器的组件id.
- 两者二选一.
- condition: 指定符合条件的情况下, 才缓存.
- cacheManager: 指定缓存管理器, 或cacheResolver指定缓存解析器.
- unless: 否定缓存, 当unless指定的条件为true, 方法的返回值就不会被缓存. 可以获取到结果进行排查.
- sync: 是否使用异步模式, 默认为false, 若使用, 则unless属性不支持.
- 举例(service方法中)
- 使用key
//condition = "#a0<2": 第一个参数的值<2时才进行缓存. //unless = "#a0==2" 如果第一个参数的值为2, 其结果不缓存 @Cacheable(cacheNames = {"emp"}, key = "#root.methodName + '[' + #id + ']'", condition = "#a0<2 and #root.methodName eq 'getEmp'", unless = "#a0==2") public Employee getEmp(Integer id) { System.out.println("查询" + id + "号员工"); Employee emp = employeeMapper.getEmpById(id); return emp; }
- 使用keyGenerator
@Cacheable(cacheNames = {"emp"}, keyGenerator = "myKeyGenerator") public Employee getEmp(Integer id) { System.out.println("查询" + id + "号员工"); Employee emp = employeeMapper.getEmpById(id); return emp; }
- 需要自己手写KeyGenerator的实现加入容器中
@Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object o, Method method, Object... objects) { return method.getName() + "[" + Arrays.asList(objects).toString() + "]"; } }; } }
- 需要自己手写KeyGenerator的实现加入容器中
- 使用key
- @CachePut
/** * @CachePut: 既调用方法, 又更新缓存数据 * 1. 修改了数据库的某个数据, 同时更新缓存 * 2. 运行时机 * 1. 先调用目标方法 * 2. 再把目标方法的结果缓存起来 * 3.测试步骤 * 1. 查询1号员工, 查到的结果会放到缓存中 * key: 1; value: 张三/1 * 2. 以后查询还是之前的结果 * 3. 更新1号员工 [lastName=zhangsan; gender=0] * 将方法的返回值也放进缓存了, 但它的key是传入的employee对象, 而值是返回的employee对象 * 4. 查询1号员工 * 是更新前的员工, 为什么是没更新前的? [1号员工没在缓存中更新] * 5. 所以应手动设置key * key="#employee.id"; 或 key="#result.id" */ @CachePut(cacheNames = {"emp"}) public Employee updateEmp(Employee employee) { System.out.println("员工更新: " + employee); employeeMapper.updateEmp(employee); return employee; } 只需要在@CachePut中添加属性key, 设置相应的值即可. (查询的key和更新的key必须一致)
- @CacheEvict
/** * @CacheEvict: 清除缓存 * 1. 属性 * key: 指定要清楚的数据 * allEntries: 是否删除所有缓存. 默认为false. (只清空value/cacheNames中设置的缓存.) * beforeInvocation: 缓存的清除是否在方法之前执行, 默认是false. * 举个例子: 如果当前方法出错, 若设false, 则不会清空缓存, 若true, 则会. */ @CacheEvict(value = "emp", allEntries = true, beforeInvocation = true) public void deleteEmp(Integer id) { System.out.println("删除员工" + id); employeeMapper.deleteEmpById(id); }
- @Caching&CacheConfig
- @Caching
/** * @Caching指定多种缓存规则 */ @Caching( cacheable = { @Cacheable(value = "emp", key = "#lastName") }, put = { @CachePut(value = "emp", key = "#result.id"), @CachePut(value = "emp", key = "#result.email") } ) public Employee getEmpByLastName(String lastName) { System.out.println("根据名字查询" + lastName); Employee emp = employeeMapper.getEmpByLastName(lastName); return emp; }
- @CacheConfig
/** * 在类上加@CachConfig, 指定cacheNames, 则该类所有的缓存名均为此值. */ @CacheConfig(cacheNames = "emp") @Service public class EmployeeService { //... }
- @Caching
整合Redis
- 默认给容器中注册的CacheManager是: ConcurrentMapCacheManager
- 可以获取和创建ConcurrentMapCache类型的缓存组件, 其作用是将数据缓存在ConcurrentMap中.
- 而在开发中, 常用缓存中间件, 如Redis, EhCache等.
- 整合Redis作为缓存
- 先安装Redis, 使用Docker方法.
拉取Redis
docker pull redis:latest
运行Redis docker run -d -p 6379:6379 --name myredis docker.io/redis - 引入Redis的starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 配置redis
- application.properties中配置
spring.redis.host=192.168.13.129
- 测试字符串
//操作k-v都是String的 @Autowired private StringRedisTemplate stringRedisTemplate; /** * Redis五大数据类型 * String: stringRedisTemplate.opsForValue() 操作字符串 * List: stringRedisTemplate.opsForList() 操作列表 * Set: stringRedisTemplate.opsForSet() 操作集合 * Hash: stringRedisTemplate.opsForHash() 操作哈希 * ZSet: stringRedisTemplate.opsForZSet() 操作有序集合 * * 而redisTemplate只是泛型和stringRedisTemplate不同 */ @Test public void test01() { ValueOperations<String, String> stringOps = stringRedisTemplate.opsForValue(); //给Redis中保存数据 //stringOps.append("msg", "hello"); //读取数据 //String msg = stringOps.get("msg"); //System.out.println(msg); //列表中添加数据 ListOperations<String, String> listOps = stringRedisTemplate.opsForList(); listOps.leftPush("mylist", "1"); listOps.leftPush("mylist", "2"); }
- 测试对象
//k-v都是Object的 @Autowired private RedisTemplate redisTemplate; //测试保存对象 @Test public void test02() { //这里Employee必须序列化 //默认如果保存对象, 使用jdk序列化机制, 序列化后的数据保存到redis中 Employee emp = employeeMapper.getEmpById(1); redisTemplate.opsForValue().set("emp-01", emp); }
- 但此时保存到Redis中的是序列化数据, 很难看.
- 将对象以json形式保存
- 配置自定义序列化规则
@Configuration public class MyRedisConfig { @Bean public RedisTemplate<Object, Employee> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Employee> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(serializer); return template; } }
- 使用
@Autowired private RedisTemplate<Object, Employee> empTemplate; //测试保存对象 @Test public void test02() { Employee emp = employeeMapper.getEmpById(1); //1.将数据以json的方式保存 //redisTemplate有默认的序列化规则 //改变默认的序列化规则 empTemplate.opsForValue().set("emp-02", emp); }
- 配置自定义序列化规则
- application.properties中配置
- 测试缓存
- 原理: CacheManager -> Cache缓存组件 -> 缓存组件给缓存中CRUD数据.
- 引入redis的starter后, 容器中保存的是RedisCacheManager.
- RedisCacheManager帮我们创建RedisCache来作为缓存组件. RedisCache通过Redis缓存数据.
- 默认保存数据k-v, 都是Object, 利用序列化保存
- 如何保存为json?
- 默认创建的RedisCacheManager操作Redis时使用的RedisTemplate<Object, Object>
- 而默认使用jdk序列化机制.
- 自定义CacheManager
@Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { //初始化一个RedisCacheWriter RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //设置CacheManager的值序列化方式为json序列化 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer); RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair); //设置默认超时过期时间30秒 redisCacheConfig.entryTtl(Duration.ofSeconds(30)); //初始化RedisCacheManager return new RedisCacheManager(redisCacheWriter, redisCacheConfig); }
- 原理: CacheManager -> Cache缓存组件 -> 缓存组件给缓存中CRUD数据.
- 之后, 上述测试缓存的例子如localhost:8080/emp/1 会将结果以json的形式保存在Redis中了.