springboot 缓存管理

笔记来源:尚硅谷b站spring boot高级视频教程

springboot与缓存

JSR-107规范

为了统一缓存的开发规范、提高系统的扩展性和最小化开发成本等,J2EE 发布了 JSR-107 缓存规范。

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 设置。
    在这里插入图片描述

spring boot的缓存抽象

spring虽然遵循JSR-107规范,但是并没有完全保留它定义的所有接口,而是只保留了Cache和CacheManager。
Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager接口来统一不同的缓存技术并支持使用 JCache(JSR-107)注解简化我们开发。
在这里插入图片描述

下面我们介绍一下springboot中缓存使用的一些类及注解

Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(Cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存(一般将其标注在新增方法上)
@CacheEvict清空缓存(一般标注在删除方法上)
@CachePut保证方法被调用,又希望结果被缓存。 (一般标注在更新操作的方法上)
@EnableCaching开启基于注解的缓存(一般标注在配置类上)
keyGenerator缓存key的生成策略(下面会详细描述)
serialize缓存时的序列化策略

缓存使用案例与常用注解

参考 云–澈 同学的博客步骤创建项目

  1. 使用idea spring初始化器创建项目,选中web/mybatis/mysql/cache
  2. 编写controller service
  3. 单元测试,我们在service的方法中标注@Cacheable注解,当每次调用方法的时候都会从缓存中获取
/**
 * 将方法的运行结果进行缓存,再次运行该方法时从缓存中返回结果
 * CacheManager 管理多个 Cache 组件,Cache 组件进行缓存的 CRUD,每一个缓存组件都有唯一一个名字。
 * 属性:
 *      cacheNames/value:指定缓存组件的名字
 *      key:缓存数据键值对的 key,默认是方法的参数的值
 *      keyGenerator:key 的生成器,可以自己指定 key 的生成器的组件 id 与 key 二选一
 *      cacheManager:指定缓存管理器 cacheResolver:缓存解析器,二者二选一
 *      condition/unless(否定条件):符合指定条件的情况下才缓存
 *
 * @param id
 * @return
 */
@Cacheable(cacheNames = {"emp"}, condition = "#id % 2 == 1")
public Employee getEmp(Integer id){
    System.out.println("查询"+id+"号员工");
    Employee emp = employeeMapper.getEmpById(id);
    return emp;
}

在这里插入图片描述
在这里插入图片描述

缓存原理解析

根据上面的案例我们发现,首先我们标注了@cacheable注解,并且成功的将数据加入到了缓存,那么我们究竟是用的哪个缓存组件?redis?ehcache?
这里就要通过查看cacheAutoConfigure入手,主要看@Import注解,import注解的作用是导入,根据{}内的类,将类的返回值(也就是CacheType.values()的类型的缓存自动配置类)加入到容器中,然后让这些自动配置类自动根据condition来判断自己的组件是否生效;

这个注解使用了一个ImportSelector接口的静态内部类,并调用了这个类中的方法,这个方法的作用是将所有在CacheType.values()的数组中匹配的类型的自动配置类加入容器,然后spring boot会根据这些配置类上的condition条件注解来判断哪个缓存生效。

@Configuration
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
    value = {CacheManager.class},
    name = {"cacheResolver"}
)
@EnableConfigurationProperties({CacheProperties.class})
@AutoConfigureAfter({CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class})
public class CacheAutoConfiguration {
    public CacheAutoConfiguration() {
    }
    ....
    static class CacheConfigurationImportSelector implements ImportSelector {
        CacheConfigurationImportSelector() {
        }

        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            CacheType[] types = CacheType.values();
            String[] imports = new String[types.length];

            for(int i = 0; i < types.length; ++i) {
                imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
            }
			//这就是加入到容器中的所有缓存配置类的类名
            return imports;
        }
    }

ok,我们接着往下看,当我们知道了现在已经给容器中加入了十一个spring boot默认支持的缓存组件,那么到底是哪个组件生效了呢?有两种方法:

  1. 通过分析类头,也就是标注在配置类上面的conditional注解来分析
  2. 通过application配置文件debug=true让程序启动时打印自动配置报告 ,在这个报告中就可以看到有哪些组件生效了
    在这里插入图片描述

类头我们就 不在细说,这里通过打印自动配置报告发现,当我们没有加入任何缓存配置的时候,spring boot默认使用的就是SimpleCacheConfiguration,那么这个类干了什么呢?

@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 (ConcurrentMapCacheManager)this.customizerInvoker.customize(cacheManager);
    }
}

通过上面的代码我们可以发现,这个配置类给容器中增加了一个ConcurrentMapCacheManager这个类,通过查看代码我们发现这个类时一个Cache Manager的实现类,那么一定实现了接口中定义的getCache方法,这个方法就是根据缓存的名字获取缓存对象。我们再来看一下这个getCache方法的实现

public interface CacheManager {
    @Nullable
    Cache getCache(String var1);
    
    Collection<String> getCacheNames();
}
	private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
    @Nullable
    public Cache getCache(String name) {
    	//从cacheMap中根据名字获取缓存,如果不存在就去创建一个,这个cacheMap就是一个map,
        Cache cache = (Cache)this.cacheMap.get(name);
        if (cache == null && this.dynamic) {
            synchronized(this.cacheMap) {
                cache = (Cache)this.cacheMap.get(name);
                if (cache == null) {
                    cache = this.createConcurrentMapCache(name);
                    this.cacheMap.put(name, cache);
                }
            }
        }
        return cache;
    }
    //创建了一个ConcurrentMapCache
    protected Cache createConcurrentMapCache(String name) {
        SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;
        return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);
    }

通过上面的代码我们发现,这个ConcurrentMapCacheManager的缓存管理器的作用就是可以获取或者创建一个ConcurrentMapCache缓存对象,进入这个对象内后我们发现这个类中定义了一些缓存的增删改方法,例如put/lookup/clear/evict等等,我们进入lookup方法中看

 	@Nullable
    protected Object lookup(Object key) {
        return this.store.get(key);
    }

可以发现,就是根据key从store中拿数据,这个store是什么?就是一个CurrentMap;

总结:
这里只是例举了spring boot默认生效的缓存,也就是Cache 接口的默认实现ConcurrentMapCache和CacheManager接口的默认实现ConcurrentMapCacheManager,那么其他缓存组件也是一样的,一定会有例如RedisConfiguration,RedisCacheManager,RedisCache等等

spring boot集成redis

当我们没有指定其他缓存配置时,springboot默认使用currentMapCache,但是在开发中一般使用的其他的缓存中间件

整合步骤
  1. 引入依赖
 		<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>
  1. 配置redis,在application.yml配置文件中加入配置。当我们引入 了上面的依赖后,RedisAutoConfig自动配置类就生效了,那么这个RedisAutoConfig做了什么呢?接着往下看
spring:
	redis:
	    host: 127.0.0.1
	    port: 6379
	#    password: 123456
	    jedis:
	      pool:
	        max-active: 8
	        max-wait: -1
	        max-idle: 500
	        min-idle: 0
	    lettuce:
	      shutdown-timeout: 0

可配置的项在RedisProperties类中

@ConfigurationProperties(
    prefix = "spring.redis"
)
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
  1. 配置类中除了添加链接工厂之类的方法,主要的还有两个,就是给容器中添加了两个类,分别是RedisTemplate和StringRedisTemplate,这两个类就是用来操作redis缓存的类,类似以前的jdbcTemplate,这两个类的区别就是,一个是操作字符串,一个是操作对象。如下:
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @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
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

@Cacheable注解使用的是RedisTemplate,那么分析一下RedisTemplate

序列化配置

下面这个序列化配置并不是完美的,具体情况还要再研究。

@Configuration
@EnableCaching
public class MyRedisConfig {
	
	 @Bean
    public KeyGenerator keyGenerator(){
        return new KeyGenerator(){
            /**
             * 自定义keyGenerator
                new一个keyGenerator,这是一个接口,我们实现他的抽象发方法,这个方法的参数分别是目标对象,目标方法,和参数列表
                当我们使用@bean注解将该对象加入容器中后,在使用的时候,bean的默认id就是类名首字母小写
             */
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                return  method.getName()+"["+ Arrays.asList(objects).toString() +"]";;
            }
        };
    }

    /**
     * @bean name属性
     * 如果未指定,则bean的名称是带注解方法的名称。
     * 如果指定了,方法的名称就会忽略,如果没有其他属性声明的话,bean的名称和别名可能通过value属性配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        //使用json的方式序列化所有对象
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //创建string的序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //设置所有的key都使用string类型的序列化器
        template.setKeySerializer(stringRedisSerializer);

        //设置所有的hash类型的key也是用string类型的序列化器
        template.setHashKeySerializer(stringRedisSerializer);

        //设置hash类型的value使用json序列化器
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        //设置所有的value都使用json序列化器
        template.setValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }


    //给redisCacheManager设置序列化方式,这种方式
    //当有多个缓存管理器的时候,需要加上@Primary注解来指定一个默认的缓存管理器
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                ; //使用 Jackson2JsonRedisSerialize
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
        return redisCacheManager;
    }

}

五大数据类型操作测试

在这里插入图片描述

缓存注解测试
  • 环境

    • spring boot
    • mybatis
    • redis
  • controller

@RestController
public class EmpController {


    @Autowired
    private EmpService empService;

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    @GetMapping("/emp/{id}")
    public Employee getEmp(@PathVariable("id") Integer id){
        System.out.println("----- emp get by id-----");
        Employee emp = empService.getEmpById(id);
        return emp;
    }

    @PostMapping("/emp")
    public Map<String,Object> save(Employee employee){

        System.out.println(employee);
        empService.saveEmp(employee);
        return new HashMap<>();

    }

    //redis缓存注解使用测试
    @GetMapping("/hash")
    public String hash(){
        empService.hash();
        return  "缓存保存has成功";
    }

    //加入缓存集合
    @GetMapping("/selectEmpList")
    public String selectEmpList(){
        empService.selectEmpList();
        return  "缓存保存List成功";
    }

    //更新缓存集合
    @GetMapping("/updateEmpList")
    public String updateEmpList(@RequestParam("lastName") String lastName,@RequestParam("id") Integer id){
        empService.updateEmpList(id,lastName);
        return  "缓存更新List成功";
    }

    //删除缓存集合
    @GetMapping("/removeEmpList")
    public String removeEmpList(){
        empService.removeEmpList();
        return  "缓存删除成功成功";
    }

}
  • service
@Service
public class EmpServiceImpl extends ServiceImpl<BaseMapper<Employee>,Employee> implements EmpService{
    private static final Logger logger = LoggerFactory.getLogger(EmpServiceImpl.class);
    @Autowired
    EmpMapper empMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 1、首先要在springbootapplication类上redis配置类上添加@enableCache注解,表示启用缓存
     * @Cacheable注解属性
     *		1. cacheName或value 表示指定缓存的名字
     *		2. key 默认是参数值,指定可以使用spel表达式 
     *			- 如果我们想使用方法名+[参数值] 这种方式:key=“#root.methodName+'['+#id+']'”
     *			- 需要注意的是,不能使用#result.id 这种方式来指定key,原因是因为cacheable和cacheput的调用时机不同,cacheable要先根据key检查缓存,所以在这个时候无法获取到result,而cacheput是先执行方法,然后将方法的返回值放进缓存,因此可以使用result获取
     *		3. keyGenerator 上面的自动配置类中有自定义的生成器
     *			-  keyGenrator = “bean 的id”
     *			key和keyGenerator 二选一使用
     *		4. cacheManager 指定缓存管理器,或者cacheResolver 指定获取解析器
     *		5. condition 指定符合体哦阿健的情况下才能缓存
     *			例如:condition=‘#id>0’ 表示当id大于0的时候才缓存
     *				 condition=“#a0 > 1 ” 表示第一个参数大于1才缓存
     *				多个条件(使用spel表达式):condition="#a0 > 1 and #root.methodName eq 'aaa'"
     *		6.unless 除非的意思,例如condition=“#id>0 ” unless='id==10' 表示当id大于0时缓存,除非id==10,如果等于10则不缓存
     *		7. sync:是否异步,使用异步则unless不生效
    */			
    @Cacheable(value = "emp",key = "#id")
    public Employee getEmpById(Integer id){
        Employee emp = empMapper.getEmp(id);
        System.out.println(emp);
        return emp;
    }

    @Override
    public void saveEmp(Employee employee) {
        empMapper.insert(employee);
    }

    /**
     * 加入缓存
     * @return
     */
    @Cacheable(cacheNames = "empList", key = "11")
    @Override
    public List<Employee> selectEmpList() {
        logger.info("使用@cacheable 将list加入redis缓存");
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("gender",0);

        return super.list(queryWrapper);
    }

    /**
     * 更新缓存
     * @CachePut() 和 @Cacheable() 注解的方法返回值要一致
     * 作用:即调用方法又更新缓存,先调用目标方法,然后将目标方法的结果缓存起来
     * 要指定更新的缓存名字和key,如果没有,则新建
     */
    @CachePut(cacheNames = "empList" , key = "11")
    @Override
    public List<Employee> updateEmpList(Integer id ,String lastName ){
        Employee empById = getEmpById(id);
        empById.setLastName(lastName);
        updateById(empById);

        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("gender",0);
        return super.list(queryWrapper);
    }

	/**
	*	缓存清除
	*	不写key的情况下也是默认使用参数值
	*	allEntries:是否删除empList缓存中的所有数据,默认false
	*	beforeInvocation:缓存的清除是否在方法之前执行,默认false 在方法之后执行。之前与之后有什么区别?如果是在之前,则不论方法是否有异常,都先清除。如果在方法之后,则抛出异常后就无法删除缓存了
	*/
    @Override
    @CacheEvict(cacheNames = "empList",key = "11")
    public void removeEmpList(){

    }

    @Override
    public void hash() {
        logger.info("使用redisTemplate 操作hash类型缓存");
        HashOperations hashOperations = redisTemplate.opsForHash();
        Map<String,Object> map = new HashMap<>();
        map.put("hash1",getEmpById(1));
        map.put("hash2",getEmpById(2));

        hashOperations.putAll("redis_hash",map);
    }

}

@Cacheing和@CacheConfig注解

是一个组合注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}

测试

@service
@CacheConfig(cacheNames = "emp")//作用就是抽取公共的缓存属性
public class service{
//定义复杂缓存规则
@Caching(
        cacheable = {
            @Cacheable(cacheNames = "empList")
        },
        put = {
            //数组形式,可以指定多个规则,例如下面,就是不仅根据id缓存数据,还要根据email缓存数据
            //由于cacheable注解,因此在缓存中有key=lastName的数据,但是如果我们一直用lastName查询,则每次都会查询数据库
            //这是因为这个方法上还有cachePut注解,这个注解会保证每次方法都调用
            @CachePut(cacheNames = "empList",key = "#result.id"),
            @CachePut(cacheNames = "empList",key = "#result.email")
        },
        evict = {
            @CacheEvict()
        }
    )
    @Override
    public Employee getEmpByLastName(String lastName){
        return empMapper.getEmpByLastName(lastName);
    }
}
指定缓存key的SPEL表达式

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值