Spring Boot与缓存
缓存-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中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的
缓存-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:
项目结构
启动类:
package com.example.testcache; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @MapperScan(value = "com.example.testcache.mapper") //配置所有的Mapper @SpringBootApplication @EnableCaching//开启缓存 public class TestcacheApplication { public static void main(String[] args) { SpringApplication.run(TestcacheApplication.class, args); } }
配置文件:
spring.datasource.url=jdbc:mysql://localhost:3306/test_cache?serverTimezone=UTC 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 logging.level.com.example.testcache.mapper=debug # #debug=true # spring.redis.host=10.87.18.41
Controller:
package com.example.testcache.controller; import com.example.testcache.bean.Department; import com.example.testcache.bean.Employee; import com.example.testcache.mapper.DepartmentMapper; import com.example.testcache.mapper.EmployeeMapper; import com.example.testcache.service.DeptService; import com.example.testcache.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; //import com.example.testmybatis.mapper.EmployeeMapper; @RestController public class DeptController { @Autowired DepartmentMapper departmentMapper; @Autowired EmployeeMapper employeeMapper; @Autowired EmployeeService employeeService; @Autowired DeptService deptService; @GetMapping("/dept/{id}") public Department getDepartment(@PathVariable("id") Integer id){ return deptService.getDeptById(id); } @GetMapping("/dept") public Department insertDept(Department department){ departmentMapper.insertDept(department); return department; } @GetMapping("/emp/{id}") public Employee getEmp(@PathVariable("id") Integer id){ return employeeService.getEmpById(id); } @GetMapping("/emp") public Employee updateEmp(Employee employee){ return employeeService.updateEmp(employee); } @GetMapping("/del/{id}") public String deleteEmp(@PathVariable("id") Integer id){ employeeService.deleteEmp(id); return "success"; } @GetMapping("/emp/lastName/{lastName}") public Employee getEmpByLastName(@PathVariable("lastName") String lastName){ return employeeService.getEmpByLastName(lastName); } }
Mapper:
package com.example.testcache.mapper; import com.example.testcache.bean.Department; import org.apache.ibatis.annotations.*; //指定这是一个操作数据库的mapper @Mapper public interface DepartmentMapper { @Select("select * from department where id=#{id}") public Department getDeptById(Integer id); @Delete("delete from department where id=#{id}") public int deleteDeptById(Integer id); @Options(useGeneratedKeys = true,keyProperty = "id") @Insert("insert into department(department_name) values(#{departmentName})") public int insertDept(Department department); @Update("update department set department_name=#{departmentName} where id=#{id}") public int updateDept(Department department); }
package com.example.testcache.mapper; import com.example.testcache.bean.Employee; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; //@Mapper或者@MapperScan将接口扫描装配到容器中 @Mapper public interface EmployeeMapper { @Select("select * from employee where id=#{id}") public Employee getEmpById(Integer id); @Select("select * from employee where lastName=#{lastName}") public Employee getEmpByLastName(String lastName); @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 int deleteEmp(Integer id); }
Service:
package com.example.testcache.service; import com.example.testcache.bean.Department; import com.example.testcache.bean.Employee; import com.example.testcache.mapper.DepartmentMapper; import com.example.testcache.mapper.EmployeeMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.Cache; import org.springframework.cache.annotation.*; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.stereotype.Service; @CacheConfig(cacheNames="dept"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置 @Service public class DeptService { @Autowired DepartmentMapper departmentMapper; @Qualifier("empoyeeCacheManager") @Autowired private RedisCacheManager empoyeeCacheManager; /** * 主要针对方法配置,能够根据方法的请求参数对其结果进行缓 **/ // @Cacheable(cacheNames = {"dept"}) // @Cacheable(cacheNames = {"dept"},key="#root.methodName+'['+#id+']'") //自定义Key // @Cacheable(cacheNames = {"dept"}, keyGenerator="myKeyGenerator") //自定义Key生成器: // @Cacheable(cacheNames = {"dept"}, keyGenerator="myKeyGenerator" ,condition = "#a0>1",unless = "#a0 == 2") //自定义Key生成器 判断 >1: public Department getDeptById(Integer id){ // 内部编码方式 Department department = departmentMapper.getDeptById(id); Cache dept = empoyeeCacheManager.getCache("dept"); dept.put("dept:1",department); System.out.println("部门查询"); return department; } }
package com.example.testcache.service; import com.example.testcache.bean.Employee; import com.example.testcache.mapper.EmployeeMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.*; import org.springframework.stereotype.Service; @CacheConfig(cacheNames="employee"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置 @Service public class EmployeeService { @Autowired EmployeeMapper employeeMapper; /** * 主要针对方法配置,能够根据方法的请求参数对其结果进行缓 **/ // @Cacheable(cacheNames = {"employee"}) @Cacheable(cacheNames = {"employee"},key="#root.methodName+'['+#id+']'") //自定义Key // @Cacheable(cacheNames = {"employee"}, keyGenerator="myKeyGenerator") //自定义Key生成器: // @Cacheable(cacheNames = {"employee"}, keyGenerator="myKeyGenerator" ,condition = "#a0>1",unless = "#a0 == 2") //自定义Key生成器 判断 >1: public Employee getEmpById(Integer id){ Employee employee = employeeMapper.getEmpById(id); System.out.println("员工查询"); return employee; } /** * 保证方法被调用,又希望结果被缓存。 **/ @CachePut(value ="employee",key = "#employee.id")//key = "#employee.id":使用传入的参数的员工id // @CachePut(value ="employee",key = "#result.id")// key = "#result.id":使用返回结果集后的id public Employee updateEmp(Employee employee){ System.out.println("updateEmp:"+employee); employeeMapper.updateEmp(employee); return employee; } /** * 清空缓存 **/ @CacheEvict(value="employee",key = "#id") //删除指定key // @CacheEvict(value="employee",key = "#id", allEntries = true) //删除指定key allEntries = true:指定清除这个缓存中所有的数据 // @CacheEvict(value="employee",beforeInvocation = true/*key = "#id",*/) //删除指定key beforeInvocation 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除 public void deleteEmp(Integer id){ System.out.println("deleteEmp:"+id); // employeeMapper.deleteEmp(id); int i = 10/0; } // @Caching 定义复杂的缓存规则 @Caching( cacheable = { @Cacheable(value="employee",key = "#lastName") //员工名字 }, put = { @CachePut(value="employee",key = "#result.id"),//员工ID @CachePut(value="employee",key = "#result.email")//员工邮箱 } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); } }
@Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
原理
将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法; CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字; 原理: 1、自动配置类;CacheAutoConfiguration 2、缓存的配置类 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 3、哪个配置类默认生效:SimpleCacheConfiguration;
运行流程
4、给容器中注册了一个CacheManager:ConcurrentMapCacheManager 5、可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中; 运行流程: @Cacheable: 1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取; (CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。 2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数; key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key; SimpleKeyGenerator生成key的默认策略; 如果没有参数;key=new SimpleKey(); 如果有一个参数:key=参数的值 如果有多个参数:key=new SimpleKey(params); 3、没有查到缓存就调用目标方法; 4、将目标方法返回的结果,放进缓存中 @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存, 如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据; 核心: 1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件 2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator 几个属性: cacheNames/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存; key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 1-方法的返回值 编写SpEL; #i d;参数id的值 #a0 #p0 #root.args[0] getEmp[2] keyGenerator:key的生成器;可以自己指定key的生成器的组件id key/keyGenerator:二选一使用; cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器 condition:指定符合条件的情况下才缓存; ,condition = "#id>0" condition = "#a0>1":第一个参数的值》1的时候才进行缓存 unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断 unless = "#result == null" unless = "#a0==2":如果第一个参数的值是2,结果不缓存; sync:是否使用异步模式
自定义Key生成器:
package com.example.testcache.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{ @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator(){ @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+"["+ Arrays.asList(params).toString()+"]"; } }; } }
@CachePut:保证方法被调用,又希望结果被缓存。
/** * @CachePut:既调用方法,又更新缓存数据;同步更新缓存 * 修改了数据库的某个数据,同时更新缓存; * 运行时机: * 1、先调用目标方法 * 2、将目标方法的结果缓存起来 * * 测试步骤: * 1、查询1号员工;查到的结果会放在缓存中; * key:1 value:lastName:张三 * 2、以后查询还是之前的结果 * 3、更新1号员工;【lastName:zhangsan;gender:0】 * 将方法的返回值也放进缓存了; * key:传入的employee对象 值:返回的employee对象; * 4、查询1号员工? * 应该是更新后的员工; * key = "#employee.id":使用传入的参数的员工id; * key = "#result.id":使用返回后的id * @Cacheable的key是不能用#result * 为什么是没更新前的?【1号员工没有在缓存中更新】 * */ @CachePut(value ="employee",key = "#employee.id")//key = "#employee.id":使用传入的参数的员工id // @CachePut(value ="employee",key = "#result.id")// key = "#result.id":使用返回结果集后的id public Employee updateEmp(Employee employee){ System.out.println("updateEmp:"+employee); employeeMapper.updateEmp(employee); return employee; }
@CacheEvict:清空缓存
key:指定要清除的数据
allEntries = true:指定清除这个缓存中所有的数据
beforeInvocation = false:缓存的清除是否在方法之前执行
默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
beforeInvocation = true:
代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
/** * 清空缓存 **/ @CacheEvict(value="employee",key = "#id") //删除指定key // @CacheEvict(value="employee",key = "#id", allEntries = true) //删除指定key allEntries = true:指定清除这个缓存中所有的数据 // @CacheEvict(value="employee",beforeInvocation = true/*key = "#id",*/) //删除指定key beforeInvocation 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除 public void deleteEmp(Integer id){ System.out.println("deleteEmp:"+id); // employeeMapper.deleteEmp(id); int i = 10/0; }
@Caching:定义复杂的缓存规则
@Caching( cacheable = { @Cacheable(value="employee",key = "#lastName") //员工名字 }, put = { @CachePut(value="employee",key = "#result.id"),//员工ID @CachePut(value="employee",key = "#result.email")//员工邮箱 } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); }
@EnableCaching:开启基于注解的缓存 参考Controller层
@CacheConfig:抽取缓存的公共配置
@CacheConfig(cacheNames="employee"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置 @Service public class EmployeeService {
注意:使用异步不支持unless
缓存-整合redis实现缓存
引入spring-boot-starter-data-redis
application.yml配置redis连接地址
使用RestTemplate操作redis
redisTemplate.opsForValue(); //操作字符串
redisTemplate.opsForHash(); //操作hash
redisTemplate.opsForList(); //操作list
redisTemplate.opsForSet(); //操作set
redisTemplate.opsForZSet(); //操作有序set
配置缓存、CacheManagerCustomizers
测试使用缓存、切换缓存、 CompositeCacheManager
安装镜像:
连接:
引入redis启动器:
官网:
<!-- 引入redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置redisConfig:
package com.example.testcache.config; import com.example.testcache.bean.Employee; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import java.time.Duration; @Configuration public class MyRedisConfig { @Bean public RedisTemplate<Object, Employee> empredisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception { RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(ser); return template; } @Bean public RedisCacheManager empoyeeCacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) //失效时间 .disableCachingNullValues() //序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build(); } }
问题1:
存入redis 的数据,没有序列化?
解决:
将数据以json储存:
方法1:将数据直接转成json
方法2:在配置类配置序列化规则,加入容器
package com.example.testcache; import com.example.testcache.bean.Employee; import com.example.testcache.mapper.EmployeeMapper; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.io.Serializable; @RunWith(SpringRunner.class) @SpringBootTest class TestcacheApplicationTests{ @Autowired EmployeeMapper employeeMapper; @Autowired StringRedisTemplate stringRedisTemplate;//操作k-v字符串 @Autowired RedisTemplate redisTemplate;//k-v都是对象 @Autowired RedisTemplate<Object,Employee> employeeRedisTemplate;//k-v都是对象 @Test public void contextLoads() { Employee employee=employeeMapper.getEmpById(1); System.out.println("Message="+employee); } /**Redis常见的五大数据类型 * String(字符串〕、List(列表)、set(集合)、Hash(散列)、ZSet(有序集合) * stringRedisTemplate.opsForValue()[String(字符串〕] * stringRedisTemplate.opsForList()[List(列表)] * stringRedisTemplate.opsForHash()[set(集合)] * stringRedisTemplate.opsForSet()[Hash(散列)] * stringRedisTemplate.opsForZSet()[ZSet(有序集合)] * */ @Test public void testRedis(){ stringRedisTemplate.opsForValue().append("msg","hello"); // String msg = stringRedisTemplate.opsForValue().get("msg"); // System.out.println("Message="+msg); // stringRedisTemplate.opsForList().leftPush("mylist","1"); // stringRedisTemplate.opsForList().leftPush("mylist","2"); } @Test public void testObjectRedis(){ Employee employee=employeeMapper.getEmpById(1); redisTemplate.opsForValue().set("emp-01",employee); } }
结果:
直接使用缓存管理器
@Bean public RedisCacheManager empoyeeCacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) //失效时间 .disableCachingNullValues() //序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build(); }
问题2:版本不一致,导致无法序列化?
解决:
在springboot1.0 和springboot2.0 中默认的序列化都是使用的jdk的 Serializer 实现这个接口,jdk自带的序列化方法
在springboot1.0中如果向自定义我们呢直接创建cachemanager 然后传入redistemple模板对象, 就可以了, redistemple 模板对象中定制序列化的方式
@Bean public RedisTemplate<Object, Employee> empRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(ser); return template; }
@Bean public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){ RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate); //key多了一个前缀 //使用前缀,默认会将CacheName作为key的前缀 cacheManager.setUsePrefix(true); return cacheManager; }
springboot2.0 中,使用rediscachemanager 就可以了
@Bean public RedisCacheManager empoyeeCacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) //失效时间 .disableCachingNullValues() //序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build(); }