目录

springboot与缓存

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设置。

http://img.cana.space/picStore/20201025160803.png

spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否 已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法 并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时我们需要关注以下两点;
    • 确定方法需要被缓存以及他们的缓存策略
    • 从缓存中读取之前缓存存储的数据

http://img.cana.space/picStore/20201025160838.png

几个重要概念与缓存注解

http://img.cana.space/picStore/20201025160922.png

http://img.cana.space/picStore/20201025160934.png

http://img.cana.space/picStore/20201025161022.png

整合redis实现缓存

完整示例地址

准备事项

  • 使用spring initializer创建工程:勾选模块:
  • Developer:Lombok;Spring Configuration Processor
  • Web:Spring Web
  • SQL:Mybatis Frameworker;MySQL Driver
  • IO:Spring cache abstraction 添加spring cache抽象依赖

SpringbootCacheApplication增加允许缓存注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@EnableCaching
@MapperScan("com.eh.springbootcache.orm.dao")
@SpringBootApplication
public class SpringbootCacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootCacheApplication.class, args);
    }

}

EmployeeService.java

1
2
3
4
5
6
@Cacheable(cacheNames = {"emp"})
public Employee getEmpById(Integer id) {
    System.out.println("查询" + id + "号员工");
    Employee emp = employeeMapper.selectByPrimaryKey(id);
    return emp;
}

将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字;

配置原理

自动配置类;CacheAutoConfiguration

缓存的配置类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
8 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"
9 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"

哪个配置类默认生效:SimpleCacheConfiguration;

1
2
3
   SimpleCacheConfiguration matched:
      - Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
      - @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)

SimpleCacheConfiguration给容器中注册了一个CacheManager: ConcurrentMapCacheManager

可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中;

运行流程

以 @Cacheable为例:

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

    org.springframework.cache.concurrent.ConcurrentMapCacheManager#getCache

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    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;
    }
    
  2. 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;

    org.springframework.cache.interceptor.CacheAspectSupport#findCachedItem

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
       Object result = CacheOperationExpressionEvaluator.NO_RESULT;
       for (CacheOperationContext context : contexts) {
          if (isConditionPassing(context, result)) {
             Object key = generateKey(context, result);
             Cache.ValueWrapper cached = findInCaches(context, key);
             if (cached != null) {
                return cached;
             }
             else {
                if (logger.isTraceEnabled()) {
                   logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
                }
             }
          }
       }
       return null;
    }
    

    key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    /**
     * Generate a key based on the specified parameters.
     */
    public static Object generateKey(Object... params) {
       if (params.length == 0) {
          return SimpleKey.EMPTY;
       }
       if (params.length == 1) {
          Object param = params[0];
          if (param != null && !param.getClass().isArray()) {
             return param;
          }
       }
       return new SimpleKey(params);
    }
    

    SimpleKeyGenerator生成key的默认策略;

    • 如果没有参数;key=new SimpleKey();
    • 如果有一个参数:key=参数的值
    • 如果有多个参数:key=new SimpleKey(params);
  3. 没有查到缓存就调用目标方法;

  4. 将目标方法返回的结果,放进缓存中

小结:

  • 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
  • key使用keyGenerator生成的,默认是SimpleKeyGenerator
  • @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据;

@Cacheable注解

cacheNames/value:指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存;

key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值作为key,方法的返回值作为值

编写SpEL; #id;参数id的值 #a0 #p0 #root.args[0]

eg: 方法名[id的值]

1
2
@Cacheable(cacheNames = {"emp"}, key = "#root.methodName+'['+ #id +']'")
public Employee getEmpById(Integer id) {

keyGenerator:key的生成器;可以自己指定key的生成器的组件id;key/keyGenerator:二选一使用;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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()+"]";
            }
        };
    }
}

使用的时候值填bean的id即可

1
2
@Cacheable(cacheNames = {"emp"}, keyGenerator = "myKeyGenerator")
public Employee getEmpById(Integer id) {

cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器

condition:指定符合条件的情况下才缓存;

1
2
3
// 第一个参数的值>0并且<2的时候才进行缓存
@Cacheable(cacheNames = {"emp"}, condition = "#a0 >0 and #a0 < 2")
public Employee getEmpById(Integer id) {

unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断

eg:

1
2
unless = "#result == null"
unless = "#a0==2":如果第一个参数的值是2结果不缓存

sync:是否使用异步模式,默认是方法执行完以同步的方式将方法返回结果保存在缓存中,将sync设置为true可以调成异步模式。

注意:调成异步模式后unless写法就不支持了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * Synchronize the invocation of the underlying method if several threads are
 * attempting to load a value for the same key. The synchronization leads to
 * a couple of limitations:
 * <ol>
 * <li>{@link #unless()} is not supported</li>
 * <li>Only one cache may be specified</li>
 * <li>No other cache-related operation can be combined</li>
 * </ol>
 * This is effectively a hint and the actual cache provider that you are
 * using may not support it in a synchronized fashion. Check your provider
 * documentation for more details on the actual semantics.
 * @since 4.3
 * @see org.springframework.cache.Cache#get(Object, Callable)
 */
boolean sync() default false;

@CachePut注解

即调用方法,又更新缓存数据;常见场景是修改了数据库的某个数据,同时更新

1
2
3
4
5
6
@CachePut(value = "emp", key = "#employee.id")
public Employee updateEmp(Employee employee) {
    System.out.println("updateEmp:" + employee);
    employeeMapper.updateByPrimaryKeySelective(employee);
    return employee;
}

运行时机:

  1. 先调用目标方法
  2. 再将目标方法的结果缓存起来

测试步骤:

  1. 查询员工,此时缓存中有员工数据
  2. 更新员工,覆盖key对应的数据
  3. 查询员工,更新后的数据

注意事项:

@CachePut中key可以使用返回值(#root)获取值,因为总是在执行后放入值,而@Cacheable只能使用入参的值,因为是先使用key再去调方法。

@CacheEvict注解

1
2
3
4
5
@CacheEvict(value = "emp", key = "#id")
public void deleteEmpById(Integer id) {
    System.out.println("deleteEmp:" + id);
    employeeMapper.deleteByPrimaryKey(id);
}
  • key:指定要清除的数据
  • allEntries = true:指定清除emp这个缓存中所有的数据
  • beforeInvocation = false:缓存的清除是否在方法之前执行 默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
  • beforeInvocation = true: 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除

@Caching

可以使用@Caching定义复杂的缓存注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {

   Cacheable[] cacheable() default {};

   CachePut[] put() default {};

   CacheEvict[] evict() default {};

}

可以利用Cacheable/CachePut/CacheEvict的运行时机构造复杂的缓存,eg:

1
2
3
4
5
6
7
8
9
@Caching(
         cacheable = {
             @Cacheable(/*value="emp",*/key = "#lastName")
         },
         put = {
             @CachePut(/*value="emp",*/key = "#result.id"),
             @CachePut(/*value="emp",*/key = "#result.email")
         }
    )

@CacheConfig

将一个类中缓存的公共配置抽取出来

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
   String[] cacheNames() default {};
   String keyGenerator() default "";
   String cacheManager() default "";
   String cacheResolver() default "";
}

eg:

1
2
3
@CacheConfig(cacheNames="emp"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置
@Service
public class EmployeeService {

整合redis实现缓存

  1. 引入spring-boot-starter-data-redis

    1
    2
    3
    4
    
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. application.yml配置redis连接地址

  3. 使用RestTemplate操作redis,RedisAutoConfiguration已经自动将RedisTemplate和StringRedisTemplate放到容器中了

    1. redisTemplate.opsForValue();//操作字符串

    2. redisTemplate.opsForHash();//操作hash

    3. redisTemplate.opsForList();//操作list

      1
      
      stringRedisTemplate.opsForList().leftPush("mylist","1");
      
    4. redisTemplate.opsForSet();//操作set

    5. redisTemplate.opsForZSet();//操作有序set

  4. 自定义序列化机制

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    package com.eh.springbootcache;
       
    import com.eh.springbootcache.orm.bean.Employee;
    import com.eh.springbootcache.orm.dao.EmployeeMapper;
    import org.junit.jupiter.api.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
       
    import javax.annotation.Resource;
       
    @SpringBootTest
    class SpringbootCacheApplicationTests {
       
        private static final Logger logger = LoggerFactory.getLogger(SpringbootCacheApplicationTests.class);
       
        @Autowired
        private EmployeeMapper employeeMapper;
        /*
        注意这里如果使用@Autowired注解就不能使用泛型,因为自动装配时使用了泛型注入,RedisTemplate<Object, Object>,
        与RedisTemplate<String, Employee>类型不一样,所以要么保持一致,要么使用名称@Resource进行注入
         */
        @Resource
        private RedisTemplate<String, Employee> redisTemplate;
       
        @Test
        void contextLoads() {
            Employee employee = employeeMapper.selectByPrimaryKey(1);
            redisTemplate.opsForValue().set("emp", employee);
            Employee employee1 = redisTemplate.opsForValue().get("emp");
            logger.info("emp:{}", employee1);
        }
       
    }
    

    默认使用jdk序列化机制,我们可以定制自己的序列化机制

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    package com.eh.springbootcache.config;
       
    import com.eh.springbootcache.orm.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;
       
    @Configuration
    public class MyRedisConfig {
        @Bean
        public RedisTemplate<String, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Employee> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<>(Employee.class);
            /**
             *  @SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
             *  @SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
             *  @SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
             *  @SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
             *  上面都会采用默认的序列化器
             */
            template.setDefaultSerializer(ser);
            return template;
        }
    }
    

    现在在客户端可以看到key和value正常显示了

    1
    2
    3
    4
    5
    6
    
    127.0.0.1:6379> keys *
    1) "msg"
    2) "\"emp\""
    3) "\xac\xed\x00\x05t\x00\x03emp"
    127.0.0.1:6379> get "\"emp\""
    "{\"id\":1,\"lastName\":\"tom\",\"email\":\"tom@gmail.com\",\"gender\":1,\"dId\":1}"
    
  5. 配置缓存、RedisTemplate与CacheManager

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    
    package com.eh.springbootcache.config;
       
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.JsonTypeInfo;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
    import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
    import org.springframework.cache.CacheManager;
    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.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
       
    import java.time.Duration;
       
    @Configuration
    public class MyRedisConfig {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            // 我们为了自己开发方便,一般直接使用 <String, Object>
            // 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object>
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            setRedisTemplate(template);
            template.afterPropertiesSet();
            return template;
        }
       
        private void setRedisTemplate(RedisTemplate<String, Object> template) {
            // Json序列化配置
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
       
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            // 解决jackson2无法反序列化LocalDateTime的问题
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            objectMapper.registerModule(new JavaTimeModule());
       
            // 该方法过时
            // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            // 上面 enableDefaultTyping 方法过时,使用 activateDefaultTyping
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
       
            // String 的序列化
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            // 设置值(value)的序列化采用FastJsonRedisSerializer。
            // 设置键(key)的序列化采用StringRedisSerializer。
            template.afterPropertiesSet();
        }
       
       
        /**
         * 自定义spring缓存抽象,更好地支持使用JCache(JSR-107)注解简化我们开发
         * 2.x废弃了1.x采用的使用RedisTemplate作为参数构造CacheManager,CacheManager与RedisTemplate无关
         * 默认使用CacheKeyPrefix cacheName -> cacheName::key
         * @param connectionFactory
         * @return
         */
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
       
            //解决查询缓存转换异常的问题
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jackson2JsonRedisSerializer.setObjectMapper(om);
       
            // 配置序列化(解决乱码的问题),过期时间30秒
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(30))
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                    .disableCachingNullValues();
       
            RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(config)
                    .build();
            return cacheManager;
        }
    }
    

    效果如下:

    1
    2
    3
    4
    
    127.0.0.1:6379> keys *
    1) "emp::2"
    127.0.0.1:6379> get emp::2
    "{\"@class\":\"com.eh.springbootcache.orm.bean.Employee\",\"id\":2,\"lastName\":\"jimmy\",\"email\":\"jimmy@gmail.com\",\"gender\":0,\"dId\":1}"
    
  6. 使用编码方式进行开发

    现在我们可以使用自定义缓存管理器进行缓存注解开发,也可以使用编码的方式进行开发,如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    package com.eh.springbootcache.service;
       
    import com.eh.springbootcache.orm.bean.Department;
    import com.eh.springbootcache.orm.dao.DepartmentMapper;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.Cache;
    import org.springframework.cache.CacheManager;
    import org.springframework.stereotype.Service;
       
       
    @Service
    public class DeptService {
       
        private static final Logger LOGGER = LoggerFactory.getLogger(DeptService.class);
       
        @Autowired
        DepartmentMapper departmentMapper;
       
        @Autowired
        CacheManager cacheManager;
       
        /**
         * 使用编码方式开发
         *
         * @param id
         * @return
         */
        public Department getDeptById(Integer id) {
            LOGGER.info("查询部门:{}", id);
            //获取某个缓存
            Cache cache = cacheManager.getCache("dept");
            Department department = cache.get(id, Department.class);
            if (department != null) {
                return department;
            }
            department = departmentMapper.selectByPrimaryKey(id);
            cache.put(id, department);
            return department;
        }
    }
    
  7. 给不同的key设置不同的过期时间

    属性读取文件:RedisCacheExpireConfigProperties.java

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    package com.eh.springbootcache.config;
       
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
       
    import java.time.Duration;
    import java.util.HashMap;
    import java.util.Map;
       
    @ConfigurationProperties(prefix = "spring.redis")
    @Data
    public class RedisCacheExpireConfigProperties {
        private final Map<String, Duration> cacheExpire = new HashMap<>();
    }
    

    在yml文件中配置cache-expire

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    spring:
      datasource:
        #   数据源基本配置
        username: root
        password: 333
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&allowMultiQueries=true
        # 是否执行初始化sql脚本
        initialization-mode: always
        schema:
          - classpath:sql/department.sql
          - classpath:sql/employee.sql
      redis:
        host: localhost
        port: 6379
        cache-expire:
          dept: 100s
          emp: 200s
    

    在自定义CacheManager中使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    
    package com.eh.springbootcache.config;
       
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.JsonTypeInfo;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
    import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.cache.CacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.CacheKeyPrefix;
    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.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
       
    import java.time.Duration;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;
       
    @Configuration
    @EnableConfigurationProperties(RedisCacheExpireConfigProperties.class)
    public class MyRedisConfig {
       
        private static final String SYSTEM_CACHE_REDIS_KEY_PREFIX = "springboot-demo";
       
        @Autowired
        private RedisCacheExpireConfigProperties redisCacheExpireConfigProperties;
       
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            // 我们为了自己开发方便,一般直接使用 <String, Object>
            // 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object>
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            setRedisTemplate(template);
            template.afterPropertiesSet();
            return template;
        }
       
        private void setRedisTemplate(RedisTemplate<String, Object> template) {
            // Json序列化配置
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
       
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            // 解决jackson2无法反序列化LocalDateTime的问题
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            objectMapper.registerModule(new JavaTimeModule());
       
            // 该方法过时
            // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            // 上面 enableDefaultTyping 方法过时,使用 activateDefaultTyping
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
       
            // String 的序列化
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            // 设置值(value)的序列化采用FastJsonRedisSerializer。
            // 设置键(key)的序列化采用StringRedisSerializer。
            template.afterPropertiesSet();
        }
       
       
        /**
         * 自定义spring缓存抽象,更好地支持使用JCache(JSR-107)注解简化我们开发
         * 2.x废弃了1.x采用的使用RedisTemplate作为参数构造CacheManager,CacheManager与RedisTemplate无关
         * 默认使用CacheKeyPrefix cacheName -> cacheName::key
         *
         * @param connectionFactory
         * @return
         */
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory connectionFactory, CacheKeyPrefix cacheKeyPrefix) {
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
       
            //解决查询缓存转换异常的问题
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jackson2JsonRedisSerializer.setObjectMapper(om);
       
       
            // 配置序列化(解决乱码的问题),过期时间30秒
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                    .disableCachingNullValues()
                    .computePrefixWith(cacheKeyPrefix); // 使用自定义cache前缀
       
            // 给各个cache设置过期时间
            Set<String> cacheNames = new HashSet<>();
            Map<String, RedisCacheConfiguration> cacheExpireConfig = new ConcurrentHashMap<>();
            for (Map.Entry<String, Duration> entry : redisCacheExpireConfigProperties.getCacheExpire().entrySet()) {
                cacheNames.add(entry.getKey());
                cacheExpireConfig.put(entry.getKey(), config.entryTtl(entry.getValue()));
            }
       
       
            RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(config)
                    .initialCacheNames(cacheNames)
                    .withInitialCacheConfigurations(cacheExpireConfig)
                    .build();
            return cacheManager;
        }
       
        @Bean
        public CacheKeyPrefix cacheKeyPrefix() {
            return cacheName -> {
                StringBuilder sb = new StringBuilder();
                sb.append(SYSTEM_CACHE_REDIS_KEY_PREFIX).append(":").append(cacheName).append(":");
                return sb.toString();
            };
        }
    }
    
  8. 测试使用缓存、切换缓存、 CompositeCacheManager