九、SpringBoot集成缓存

1、Spring缓存

1.1、缓存依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

1.2、缓存注解

  • @EnableCaching:在主入口类上添加该注解,用于开启缓存;

  • @CacheConfig:标注在类上,表示当前类使用的缓存组件中的key为该注解指定的cacheNames/value,当该注解指定了cacheNames/value之后,@Cacheable上就无需再使用cacheNames/value了;

  • @Cacheable:将方法的结果进行缓存;

    • cacheNames/value:缓存组件的名字;

    • key:缓存数据使用的key,默认是使用方法参数的值,可以使用SpEL表达式,比如#id == #a0 == #p0 == #root.args[0]都表示使用第一个参数的值作为缓存的key;

    • keyGenerator:key的生成器,可以自己指定key的生成器的组件id;

    • cacheManager:指定缓存管理器;

    • cacheResolver:指定获取解析器;

    • condition:指定符合条件的情况下才缓存,支持SpEL表达式;

      • condition=“#id>1”:表示id的值大于1时才缓存。
    • unless:否定缓存,当unless指定的条件为true,方法的返回值就不会被缓存,该属性可以获取到结果进行判断,unless与condition刚好相反;

      • unless=“#a0==2”:表示第一个参数的值为2时,查询结果不缓存。
    • sync:是否使用异步模式,使用sync后unless就不支持了;

      其中keyGeneratorkey二者只能选一,cacheResolvercacheManager也二者选一。

  • @CachePut:用于更新缓存,它先调用方法,然后将目标方法的结果进行缓存。

    ​ 能够更新缓存的前提是,@CachePut更新使用的key与@Cacheable使用的key要保持一致。

    @CachePut注解属性同@Cacheable的属性类似,少了sync属性。

  • @CacheEvict:缓存清除

    ​ 同样如果要清除缓存,则使用的key值要与@Cacheable使用的key一致,否则达不到清除缓存的功能。

    @CacheEvict注解属性比@Cacheable注解少了sync和unless,但是多了两个属性:allEntriesbeforeInvocation

    allEntries:表示是否删除所有缓存。

    beforeInvocation:表示删除缓存的操作是否在目标方法执行之前。

1.3、原理

CacheAutoConfiguration是缓存的自动配置类。

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

        @Override
        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;
        }
    }
}

缓存配置类(有顺序):

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

开启debug=true,查看控制台,可以发现:默认cache配置类生效的只有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 this.customizerInvoker.customize(cacheManager);
    }

}

该类给容器中注册了一个ConcurrentMapCacheManager类型的cacheManager组件。

public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);

    //other code...
    
    //从缓存ConcurrentMap对象中获取cacheNames/value指定的Cache对象。
    @Override
    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;
    }
}

该组件可以获取和创建ConcurrentMapCache类型的缓存组件,作用是将数据保存在ConcurrentMap对象中。

运行流程:

​ 1)、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字去缓存中获取(CacheManager先获取相应的缓存),第一次获取缓存,如果没有Cache组件,则会自动创建。

​ 2)、去Cache中查找缓存的内容,使用一个key(默认是方法的参数),其中key是按照某种策略生成的,默认是使用SimpleKeyGenerator生成的。

SimpleKeyGenerator生成key的策略:

​ 如果没有参数,key=new SimpleKey();

​ 如果有一个参数,key=参数值

​ 如果有多个参数,key=new SimpleKey(params);

​ 3)、没有查到缓存,就调用目标方法;如果查到缓存,则直接返回结果,不调用目标方法。

​ 4)、没有查到缓存后去调用目标方法,然后将目标方法返回的结果放进缓存。

1.4、注解解释

@Cacheable

自定义key:

​ methodName:当前被调用的方法名,例如#root.methodName

​ method:当前被调用的方法,例如#root.method.name

​ target:当前被调用的目标对象,例如#root.target

​ targetClass:当前被调用的目标对象类,例如#root.targetClass

​ args:当前被调用的方法的参数列表,例如#root.args[0]

​ caches:当前方法调用使用的缓存列表,

​ argument name:方法参数的名字,可以直接#参数名,也可以使用#a0或者#p0的形式,0表示参数索引,例如#id,#a0,#p0等。

例如:

@Cacheable(cacheNames = "emp", key = "#root.methodName+'[' + #id + ']'")

当然也可以自定义KeyGenerator:

@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
    return (target, method, params) -> method.getName() + "[" + Arrays.asList(params).toString() + "]";
}

则在@Cacheable上使用该自定义的KeyGenerator:

@Cacheable(cacheNames = "emp", keyGenerator = "myKeyGenerator")

@CachePut:同步更新缓存

它主要用于更新缓存,它先调用方法,然后将目标方法的结果进行缓存。但是能够更新缓存的前提是,@CachePut更新使用的key与@Cacheable使用的key要保持一致。

@Cacheable(cacheNames = "emp", key = "#a0")
public Employee getById(Integer id){}

@CachePut(cacheNames = "emp", key = "#result.id")
public Employee update(Employee employee){}

其中#result表示目标方法的返回值。因为@CachePut更新缓存比目标方法晚,所有@CachePut能获取到返回值。而@Cacheable比目标方法早,所以无法使用#result。

@CacheEvict:缓存清除

同样如果要清除缓存,则使用的key值要与@Cacheable使用的key一致,否则达不到清除缓存的功能。

​ 属性allEntries,默认为false,表示不把cacheNames里所有的缓存都删除掉。当该值为true的时候,会把缓存中对应的cacheNames里的所有缓存都删除。

​ 属性beforeInvocation,默认为false,表示删除缓存的操作在目标方法执行之后。当该值为true的时候,则删除缓存的操作在目标方法执行之前。区别:如果设为true,当目标方法执行出现异常后,对应的缓存已经被删除了。如果设为false(默认),当目标方法执行出现异常后,就不会把缓存删除掉。

2、Redis缓存

2.1、redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2、redis自动配置类

@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
    
    @Configuration
    @ConditionalOnClass(GenericObjectPool.class)
    protected static class RedisConnectionConfiguration {
        
        //other code...
        
        @Bean
        @ConditionalOnMissingBean(RedisConnectionFactory.class)
        public JedisConnectionFactory redisConnectionFactory()
                throws UnknownHostException {
            return applyProperties(createJedisConnectionFactory());
        }
        
        //other code...
        
    }
    
    @Configuration
    protected static class RedisConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "redisTemplate")
        public RedisTemplate<Object, Object> redisTemplate(
                RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
            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;
        }

    }
}

在RedisAutoConfiguration自动配置类中,注册了几个重要的Bean,分别是JedisConnectionFactoryRedisTemplateStringRedisTemplate。可以在类中直接注入RedisTemplateStringRedisTemplate

2.3、redis常用数据类型

String——字符串

List——列表

Set——集合

Hash——散列

ZSet——有序集合

具体命令可参考Redis命令

2.4、使用redis序列化对象

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Employee> employeeRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();

        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<>(Employee.class);
        template.setDefaultSerializer(serializer);
        return template;
    }
}

使用该redisTemplate:

@Autowired
private RedisTemplate<Object, Employee> employeeRedisTemplate;

@Test
public void test02(){
    Employee e = employeeService.getById(1);
    //默认保存对象,使用jdk序列化机制,序列化后的数据保存到redis中
    employeeRedisTemplate.opsForValue().set("emp-01", e);

    Employee employee = employeeRedisTemplate.opsForValue().get("emp-01");
    System.out.println(employee);
}

2.5、redis缓存

一旦引入了redis的starter之后,cache的配置类就变成了RedisCacheConfiguration

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

    private final CacheProperties cacheProperties;

    private final CacheManagerCustomizers customizerInvoker;

    RedisCacheConfiguration(CacheProperties cacheProperties,
            CacheManagerCustomizers customizerInvoker) {
        this.cacheProperties = cacheProperties;
        this.customizerInvoker = customizerInvoker;
    }

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        cacheManager.setUsePrefix(true);
        List<String> cacheNames = this.cacheProperties.getCacheNames();
        if (!cacheNames.isEmpty()) {
            cacheManager.setCacheNames(cacheNames);
        }
        return this.customizerInvoker.customize(cacheManager);
    }

}

而CacheManager就变成了RedisCacheManager

//加载缓存
@Override
protected Collection<? extends Cache> loadCaches() {

    Assert.notNull(this.redisOperations, "A redis template is required in order to interact with data store");

    Set<Cache> caches = new LinkedHashSet<Cache>(
        loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : new ArrayList<Cache>());

    Set<String> cachesToLoad = new LinkedHashSet<String>(this.configuredCacheNames);
    cachesToLoad.addAll(this.getCacheNames());

    if (!CollectionUtils.isEmpty(cachesToLoad)) {

        for (String cacheName : cachesToLoad) {
            caches.add(createCache(cacheName));
        }
    }

    return caches;
}

protected RedisCache createCache(String cacheName) {
    long expiration = computeExpiration(cacheName);
    return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
                          cacheNullValues);
}

RedisCacheManager通过创建RedisCache来作为缓存组件,然后通过RedisCache来进行缓存的操作。

其中在创建RedisCacheManager组件的时候传入的是RedisTemplate<Object, Object>,而它默认使用的是jdk的序列化机制。

public RedisTemplate() {}

public void afterPropertiesSet() {

    super.afterPropertiesSet();

    boolean defaultUsed = false;

    if (defaultSerializer == null) {

        defaultSerializer = new JdkSerializationRedisSerializer(
            classLoader != null ? classLoader : this.getClass().getClassLoader());
    }

    if (enableDefaultSerializer) {

        if (keySerializer == null) {
            keySerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (valueSerializer == null) {
            valueSerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (hashKeySerializer == null) {
            hashKeySerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (hashValueSerializer == null) {
            hashValueSerializer = defaultSerializer;
            defaultUsed = true;
        }
    }

    if (enableDefaultSerializer && defaultUsed) {
        Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
    }

    if (scriptExecutor == null) {
        this.scriptExecutor = new DefaultScriptExecutor<K>(this);
    }

    initialized = true;
}

问题:如何将RedisCacheManager使用json序列化机制呢?

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Employee> employeeRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();

        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<>(Employee.class);
        template.setDefaultSerializer(serializer);
        return template;
    }

    //自定义缓存管理器CacheManager
    @Bean
    @Primary
    public RedisCacheManager employeeRedisCacheManager(@Autowired RedisTemplate<Object, Employee> employeeRedisTemplate){
        RedisCacheManager manager = new RedisCacheManager(employeeRedisTemplate);
        //使用前缀,会使key多一个前缀,默认会使用cacheNames作为key的前缀
        manager.setUsePrefix(true);
        return manager;
    }

}

但是该RedisCacheManager只能操作Employee,如果使用该RedisCacheManager来操作其他Object,就会报错,所以通常情况下,不应该修改默认的RedisCacheManager。

如果有多个CacheManager缓存管理器,则可以在@CacheConfig或者@Cacheable中的属性cacheManager上指定所使用的CacheManager。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容