avatar

17.SpringBoot整合之缓存篇(3)

上篇我们粗略的介绍了下Spring的缓存抽象。知道了如果不引入任何缓存的依赖,默认使用的是ConcurrentHashMap进行缓存。接下来我们介绍下SpringBoot与Redis的整合。

一、快速上手

注意:因为篇(其)幅(是)原(是)因(懒),Redis环境搭建就不说了,我这里假设大家都搭建好了Redis环境。

  1. 在创建Spring项目的时候,勾选redis。SpringBoot会自动帮我们引入Redis的起步依赖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <dependencies>
    <!-- 引入Redis的起步依赖 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>
  2. 配置Redis。我这边只配置Redis的地址、端口。其他的暂时不配置。
    1
    2
    3
    4
    spring:
    redis:
    host: 192.168.25.151 # Redis地址
    port: 6379 #redis 端口
  3. 既然依赖有了。配置文件也有了。那就开始coding吧。这里我们使用两种方法,即使用RedisTemplateSpring缓存抽象
    1. 公共部分(包含Controller、dao)
      • Controller
      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
      @RestController
      @RequestMapping("/user")
      public class UserController {
      @Autowired
      private UserService userService;

      //这个请求是测试Spring缓存抽象
      @RequestMapping("/getUser/{id}")
      public User getUser(@PathVariable("id") Integer id){
      return userService.getUser(id);
      }

      //这个请求是测试RedisTemplate
      @RequestMapping("/getUser1/{id}")
      public User getUser1(@PathVariable("id") Integer id){
      return userService.getUser1(id);
      }

      @RequestMapping("/updateUser")
      public User updateUser(User user){
      return userService.updateUser(user);
      }

      @RequestMapping("/deleteUser/{id}")
      public User deleteUser(@PathVariable("id") Integer id){
      return userService.deleteUser(id);
      }
      }
      • dao(为了方便,我就不查数据库了。自己构造个)
        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
        @Repository
        public class UserDao {
        private static Map<Integer,User> map = new HashMap<>();
        static{
        map.put(1,new User(1,"张三",18));
        map.put(2,new User(2,"李四",19));
        map.put(3,new User(3,"王五",17));
        map.put(4,new User(4,"赵六",14));
        map.put(5,new User(5,"王小二",20));
        map.put(6,new User(6,"李小四",29));
        map.put(7,new User(7,"赵明",34));
        map.put(8,new User(8,"王柳",16));
        map.put(9,new User(9,"李斯",17));
        map.put(10,new User(10,"孙福",10));
        }

        public User getUser(int id){
        System.out.println("use db select, id is " +id);
        return map.get(id);
        }

        public User updateUser(User user) {
        System.out.println("update User "+ user);
        map.put(user.getId(),user);
        return map.get(user.getId());
        }

        public User deleteUser(Integer id) {
        System.out.println("delete user id is "+ id);
        return map.remove(id);
        }
        }
    2. 缓存部分(包含Service)
      1. 使用RedisTemplate
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
            @Autowired
      private RedisTemplate<Object,Object> template; //注意。使用AutoWired注入时,类型要与容器中的Bean类型一致。

      public User getUser1(Integer id){
      System.out.println("使用RedisTemplate");
      //使用opsForValue方法操作Redis的strings类型,即key,value结构的数据
      //从redis取指定的key值。如果有值就返回。没值就执行方法
      if(template.opsForValue().get(id+"") !=null){
      return (User)template.opsForValue().get(id+"");
      }
      User user = userDao.getUser(id);
      if(user!=null){
      //如果方法返回的值不为空,就放到Redis中
      template.opsForValue().set(id+"",user);
      }
      return user;
      }
      1. 使用Spring缓存抽象
      1
      2
      3
      4
      5
      6
            //注意:如果想要启用缓存抽象的注解的话,需要在启动类上加上@EnableCaching注解。
      //Spring已经帮我们统一管理。所以我们只需要之前的注解即可。
      @Cacheable(value = "UserCache",key="#id")
      public User getUser(Integer id){
      return userDao.getUser(id);
      }
  4. 测试
    • 结果我们就不说了。我们来看看在Redis中数据是怎么存的吧。
      • 看图我们可以得出几个结论:
        • 可见使用RedisTemplate和Spring缓存抽象,不是存储在一起的。这是因为Spring缓存抽象,会帮我们加一个前缀。这里在下面的源码分析中会说到。
        • 看value可以发现,我们根本看不懂存的是啥。这是因为默认使用的是java序列化机制。当然也可以定义我们自己的机制。比如json。这个在后面会说到。

二、原理初探

  1. 先说Spring缓存抽象这块吧。前两篇已经介绍得差不多了。

    1. 在SpringBoot启动的时候,会自动执行CacheConfigurationImportSelectorselectImports()方法,获取CacheConfiguration类。这里我们因为引入了Redis的依赖。那么,这里我们使用的CacheConfiguration就是RedisCacheConfiguration。那么我们就进去看看。

    2. RedisCacheConfiguration类分析

      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
      class RedisCacheConfiguration {
      private final CacheProperties cacheProperties;
      private final CacheManagerCustomizers customizerInvoker;
      private final org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration;
      //注入配置文件、定制器、RedisCacheConfiguration
      RedisCacheConfiguration(CacheProperties cacheProperties,
      CacheManagerCustomizers customizerInvoker,
      ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration) {
      this.cacheProperties = cacheProperties;
      this.customizerInvoker = customizerInvoker;
      this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
      }

      @Bean
      //创建CacheManger.注入ConnectionFactory
      public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
      ResourceLoader resourceLoader) {
      RedisCacheManagerBuilder builder = RedisCacheManager
      .builder(redisConnectionFactory) //传入ConnectionFactory
      //设置默认的RedisCacheConfiguration。determineConfiguration()方法会在下面分析
      .cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
      //获取配置的缓存名。
      List<String> cacheNames = this.cacheProperties.getCacheNames();
      if (!cacheNames.isEmpty()) {
      builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
      }
      //执行定制器,并返回执行完后的RedisCacheManager对象
      return this.customizerInvoker.customize(builder.build());
      }

      private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
      ClassLoader classLoader) {
      if (this.redisCacheConfiguration != null) {
      return this.redisCacheConfiguration;
      }
      //获取redis的配置信息
      Redis redisProperties = this.cacheProperties.getRedis();
      //先获取默认的CacheConfiguration
      org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
      .defaultCacheConfig();
      //设置value的序列化机制。这里使用的是JDK的序列化机制。我们可以改成Json的
      config = config.serializeValuesWith(SerializationPair
      .fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
      //获取配置,如果不为空就设置
      if (redisProperties.getTimeToLive() != null) {
      config = config.entryTtl(redisProperties.getTimeToLive());
      }
      //这里就是设置key的前缀。因为我们没有配置。所以在redis存的就是没有前缀的。
      if (redisProperties.getKeyPrefix() != null) {
      config = config.prefixKeysWith(redisProperties.getKeyPrefix());
      }
      if (!redisProperties.isCacheNullValues()) {
      config = config.disableCachingNullValues();
      }
      //是否启用Redis Key的前缀redisProperties的UseKeyPrefix属性默认值为true
      if (!redisProperties.isUseKeyPrefix()) {
      config = config.disableKeyPrefix();
      }
      return config;
      }

      }
    3. 至此,我们已经获取到了CacheManager。接下来就和之前一样了。在这里我们分析下RedisCache.分析下它是怎么存取的。

      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
      @Override
      //和之前的机制一样,先调用lookup方法看是否有值,如果有就返回,如果没有就返回null。随后会执行方法。
      protected Object lookup(Object key) {
      //通过缓存名和转化过的key去redis中取值
      byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
      //如果没值就返回null
      if (value == null) {
      return null;
      }
      //有值就返回解析后的值
      return deserializeCacheValue(value);
      }
      //分析下createAndConvertCacheKey。看看是key的是怎么生成的。
      private byte[] createAndConvertCacheKey(Object key) {
      //调用createCacheKey创建key。再通过Redis序列化Key的机制序列化
      return serializeCacheKey(createCacheKey(key));
      }

      //设置创建key的代码
      protected String createCacheKey(Object key) {
      //把key转成String类型。如果可以转就转成String。不能转就调用toString方法
      String convertedKey = convertKey(key);
      //判断是否开启前缀。默认是开启
      if (!cacheConfig.usePrefix()) {
      return convertedKey;
      }
      //返回组装了的前缀的key
      return prefixCacheKey(convertedKey);
      }
      //组装key
      private String prefixCacheKey(String key) {

      // allow contextual cache names by computing the key prefix on every call.
      //把缓存名传进去。里面的操作为,如果定义了前缀,就返回前缀。如果没有定义前缀就默认使用"缓存名::"作为前缀
      return cacheConfig.getKeyPrefixFor(name) + key;
      }
    4. 现在让我们总结下吧。

      1. RedisCacheConfiguration会帮我们使用org.springframework.data.redis.cache.RedisCacheConfiguration(注:该类和我们分析的类不是同一个)创建一个RedisCacheManager.
      2. 在创建org.springframework.data.redis.cache.RedisCacheConfiguration时。会设置value的序列化方式,默认为使用JDK的序列化机制。会帮我们设置前缀和缓存名。
      3. key的生成策略是先判断是否定义了前缀。如果定义了前缀就返回前置。否则返回缓存名::。再拼上我们传进来的key。得到最终存到Redis中的key
  2. 接下来,我们就看看RedisTemplate

    1. 除了CacheAutoConfiguration之外,Redis也有一个自动配置类。下面我们看看他在自动配置类中做了什么。
    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
    @Configuration
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    //导入LettuceJedis的配置类。两种只有一种生效。在配置类中建立RedisConnectionFactory。默认为Lettuce
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {

    @Bean
    //往容器中加入RedisTemplate。操作<object,object>
    @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
    //创建StringRedisTemplate。专门用来操作<string,string>
    public StringRedisTemplate stringRedisTemplate(
    RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }

    }
    1. 这样我们就可以直接在service中使用自动注入来使用RedisTemplate。但是需要注意的是。如果使用的@Autowired.则声明的属性类型一定要与容器中的类型一致。例如,在容器中声明的是RedisTemplate<Object,Object>.则在service中也要是对应的RedisTemplate<Object,Object>。否则会报错。这是因为@Autowired是按类型进行注入。可以使用@Resource进行注入。就不会有这种问题。

三、如何定制?

  1. Spring缓存抽象中RedisManager的定制
    1. 在我们的配置类中,重新定义RedisCacheManager.这里以修改默认的value的序列化机制为json为例。代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Configuration
      public class RedisConfig {
      @Bean
      public RedisCacheManager redisCacheManager(LettuceConnectionFactory factory){
      //获取默认的RedisCacheConfiguration
      RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
      //设置value序列化为jackson
      configuration = configuration.serializeValuesWith(SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
      //创建RedisCacheManager对象
      return RedisCacheManager.builder(factory).cacheDefaults(configuration).build();
      }
      }
  2. RedisTemplate的定制。
    1. 只需要重新定义下RedisTemplate即可。代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Configuration
      public class RedisConfig{

      @Bean
      public RedisTemplate<String,User> redisTemplate(RedisConnectionFactory redisConnectionFactory){
      RedisTemplate<String,User> template = new RedisTemplate<>();
      //设置Redis连接工厂
      template.setConnectionFactory(redisConnectionFactory);
      //设置Redis Key的序列化机制
      template.setKeySerializer(new StringRedisSerializer());
      //设置Redis value的序列化机制为Json
      template.setValueSerializer(new Jackson2JsonRedisSerializer(User.class));
      return template;
      }
      }

四、总结

  1. 整合步骤:
    1. 引入起步依赖
    2. 编写配置文件,配置redis的host和端口
    3. 使用
  2. 源码总结
    1. 使用CacheManager
      1. SpringBoot在启动时,会执行RedisConfiguration。为我们创建RedisManager。默认value序列化使用的是JDK的序列化。
      2. 接下来就可以通过抽象注解使用了。使用方法还是和前面使用ConcurrentHashMap一样。默认会为我们加上缓存名::的前缀
    2. 使用RedisTemplate
      1. 在SpringBoot启动时。会在RedisAutoConfiguration中,为我们创建RedisTemplateStringRedisTemplate.
      2. 我们可以在service或其他需要缓存的地方,使用@Autowired把RedisTemplate注入进来。

以上就是所有内容了。缓存篇也到此结束,下面带来的是SpringBoot与消息中间件的整合。咱们下篇再见~感谢观看!


评论