SpringBoot2.X整合Redis緩存

JAVA && Spring && SpringBoot2.x — 學習目錄

Springboot拋棄了繁雜的xml配置,采用了自動裝配的原理,所以我們看上去,只是配置了yml文件,就完成了繁雜bean的創(chuàng)建。

1. 為什么要自定義RedisTemplate

我們在代碼中,可以完成RedisTemplate的注入,而實際上,我們只是單純的配置了yml文件,在哪里創(chuàng)建了redisTemplate這個bean對象呢?

redisTemplate自動裝配的源碼:

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class 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;
    }

}

通過源碼可以看出,SpringBoot自動幫我們在容器中生成了一個RedisTemplate和一個StringRedisTemplate。

  1. RedisTemplate的泛型是<Object,Object>,寫代碼不是很方便,一般我們的key是String類型,我們需要一個<String,Object>的泛型。
  2. RedisTemplate沒有設置數據存儲在Redis時,Key和Value的序列化方式。(采用默認的JDK序列化方式)

如何優(yōu)雅的解決上述兩個問題呢?

@ConditionalOnMissing注解:如果Spring容器中已經定義了id為redisTemplate的Bean,那么自動裝配的RedisTemplate不會實例化。因此我們可以寫一個配置類,配置Redisemplate。

若未自定義RedisTemplate,默認會對key進行jdk序列化。

2. 如何自定義RedisTemplate

2.1 Redis數據的序列化問題

針對StringRedisSerializer,Jackson2JsonRedisSerializer和JdkSerializationRedisSerializer進行測試

數據結構 序列化類 序列化前 序列化后
Key/Value StringRedisSerializer test_value test_value
Key/Value Jackson2JsonRedisSerializer test_value "test_value"
Key/Value JdkSerializationRedisSerializer test_value 亂碼
Hash StringRedisSerializer 2016-08-18 2016-08-18
Hash Jackson2JsonRedisSerializer 2016-08-18 "2016-08-18"
Hash JdkSerializationRedisSerializer 2016-08-18 \xAC\xED\x00\x05t

由此可以得到結論:

  1. StringRedisSerializer進行序列化后的值,在Java和Redis中保存的內容時一致的。
  2. 用Jackson2JsonRedisSerializer序列化后,在Redis中保存的內容,比Java中多一對逗號。
  3. 用JdkSerializationRedisSerializer序列化后,對于Key-Value結構來說,在Redis中不可讀;對于Hash的Value來說,比Java的內容多了一些字符。

自定義的redisTemplate實例:

@Configuration
public class RedisConfig {
    /**
     * 由于原生的redis自動裝配,在存儲key和value時,沒有設置序列化方式,故自己創(chuàng)建redisTemplate實例
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        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);
        template.afterPropertiesSet();
        return template;
    }
}

對于Key來說,我們采用stringRedisSerializer。而對于Value來我們采用jackson2JsonRedisSerializer的序列化方式。

Jackson進行序列化

復習盤點-Java序列化方式(1)JSON序列化(溫故知新-泛型)(jdk8-LocalDate序列化)

ObjectMapper是Jackson操作的核心,Jackson所有的json操作都是在ObjectMapper中實現的。

2.2 Jackson序列化特性

1. 對于jdk1.8新時間API的序列化

在JDK1.8中的時間類,采用了一套了新的API。但是在反序列化中,會出現異常。

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of  java.time.LocalDate (no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)

在SpringBoot中的解決方案:

  • 在MAVEN中加入ckson-datatype-jsr310依賴。
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
  • 配置Configuration中的ObjectMapper。
@Bean
public ObjectMapper serializingObjectMapper() {
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  objectMapper.registerModule(new JavaTimeModule());
  return objectMapper;
}

2. 常用注解

  • @JsonIgnore 此注解用于屬性上,作用是Json操作時,忽略該屬性。
  • @JsonFormat 此注解用于屬性上, 作用是把Date類型轉化成為想要的格式。
  • @JsonProperty 此注解用于屬性上,作用是把改屬性的名稱序列化成另一個名稱。
  • @JsonSerialize 此注解用于屬性上,作用是指定屬性序列化的類型。
  • @JsonDeserialize 此注解用于屬性上,作用是指定屬性反序列化的類型。
  • @JsonInclude 屬性值為null的不參與序列化。
  • @JsonInclude(Include.NON_NULL)屬性為null,則不參與序列化。

JsonSerialize與 JsonDeserialize使用

3. 常用的redis序列化工具類

靈活的獲取Spring Bean對象

@Service
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext context;  
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         SpringContextUtil.context=applicationContext;
    }
    public static <T> T getBean(String name, Class<T> requiredType){
        return context.getBean(name, requiredType);
    }
}

Redis常用API

@Slf4j
public class RedisUtil {

    private static final RedisTemplate<String, Object> redisTemplate = SpringContextUtil.getBean("redisTemplate", RedisTemplate.class);

    /**********************************************************************************
     * redis-公共操作
     **********************************************************************************/

    /**
     * 指定緩存失效時間
     *
     * @param key  鍵
     * @param time 時間(秒)
     * @return
     */
    public static boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            log.error("【redis:指定緩存失效時間-異?!?, e);
            return false;
        }
    }

    /**
     * 根據key 獲取過期時間
     *
     * @param key 鍵 不能為null
     * @return 時間(秒) 返回0代表為永久有效;如果該key已經過期,將返回"-2";
     */
    public static long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判斷key是否存在
     *
     * @param key 鍵
     * @return true 存在 false不存在
     */
    public static boolean exists(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            log.error("【redis:判斷{}是否存在-異?!?, key, e);
            return false;
        }
    }


    /**********************************************************************************
     * redis-String類型的操作
     **********************************************************************************/

    /**
     * 普通緩存放入
     *
     * @param key   鍵
     * @param value 值
     * @return true成功 false失敗
     */
    public static boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            log.error("【redis:普通緩存放入-異常】", e);
            return false;
        }
    }

    /**
     * 普通緩存放入并設置時間
     *
     * @param key   鍵
     * @param value 值
     * @param time  時間(秒) time要大于0 如果time小于等于0 將設置無限期
     * @return true成功 false 失敗
     */
    public static boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            log.error("【redis:普通緩存放入并設置時間-異?!?, e);
            return false;
        }
    }

    /**
     * 遞增
     *
     * @param key   鍵
     * @param delta 要增加幾(大于0)
     * @return
     */
    public static long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("遞增因子必須大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 遞減
     *
     * @param key   鍵
     * @param delta 要減少幾(小于0)
     * @return
     */
    public static long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("遞減因子必須大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    /**
     * 刪除緩存
     *
     * @param key 可以傳一個值 或多個
     */
    @SuppressWarnings("unchecked")
    public static void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    /**
     * 獲取緩存
     *
     * @param key   redis的key
     * @param clazz value的class類型
     * @param <T>
     * @return value的實際對象
     */
    public  static <T> T get(String key, Class<T> clazz) {
        Object obj = key == null ? null : redisTemplate.opsForValue().get(key);
        if (!obj.getClass().isAssignableFrom(clazz)) {
            throw new ClassCastException("類轉化異常");
        }
        return (T) obj;
    }

    /**
     * 獲取泛型
     *
     * @param key 鍵
     * @return 值
     */
    public static Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
}

詳細請見 —— SpringBoot整合Redis工具類

4. jedis客戶端與lettuce客戶端

SpringBoot集成Redis主要是使用RedisTemplate類進行操作,但是在SpringBoot2.0以后,底層訪問的不再是Jedis而是lettuce。

jedis客戶端和lettuce客戶端的區(qū)別

jedis采用的是直連redis server,在多線程之間公用一個jedis實例,是線程不安全的,想要避免線程不安全,可以使用連接池pool,這樣每個線程單獨使用一個jedis實例,但是線程過多時,帶來的是redis server的負載較大。有點類似BIO模式。

lettuce采用netty連接redis server,實例在多個線程間共享,不存在線程不安全的情況,這樣可以減少線程數量。當然在特殊情況下,lettuce也可以使用多個實例,有點類似NIO模式。

 <!--redis默認使用的Lettuce客戶端-->  
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
 <!--使用默認的Lettuce時,若配置spring.redis.lettuce.pool則必須配置該依賴-->
 <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
 </dependency>

需要注意的是,若是在配置中配置了pool屬性,那么必須在pom.xml文件中加入commons-pool2的依賴。

image.png

SpringBoot2集成redis,使用lettuce客戶端

配置文件各參數的含義

如果使用lettuce客戶端每隔一段時間連接斷開怎么辦

將lettuce客戶端換成jedis客戶端

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
<!--使用redis連接池-->
 <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
 </dependency>

本文參考

redis使用Jackson2JsonRedisSerializer序列化問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容