SpringBoot2.X整合Redis缓存

JAVA && Spring && SpringBoot2.x — 学习目录

Springboot抛弃了繁杂的xml配置,采用了自动装配的原理,所以我们看上去,只是配置了yml文件,就完成了繁杂bean的创建。

1. 为什么要自定义RedisTemplate

我们在代码中,可以完成RedisTemplate的注入,而实际上,我们只是单纯的配置了yml文件,在哪里创建了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序列化方式)

如何优雅的解决上述两个问题呢?

@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时,没有设置序列化方式,故自己创建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客户端的区别

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序列化问题

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

推荐阅读更多精彩内容