SpringBoot中Redis的使用详解

在Springboot中提供了spring-data-redis操作Redis很方便,底层它提供了Jedis和Lettuce两种方式来访问Redis,通过Springboot配置就可以完成Jedis和Lettuce的集成,使得集成非常简单。同时它也提供了统一的访问类RedisTemplate来访问Redis,使得访问Redis接口统一,代码也更优雅。同时它也提供了序列化数据的方式,使得读取和存储数据更加简单,更加高效。但在使用SpringDataRedis时,也需要注意一些问题,特别是序列化引起的兼容性问题,需要多加注意。这篇文章将从各个方面详细的讲解在SpringBoot中使用Redis的注意事项。

简单的集成方式

SpringDataRedis可以使用配置文件的方式来配置使用哪种连接方式,关于这两种访问方式的差别,主要体现在多线程安全上:

  • Redis不是线程安全,在多个线程间共享一个 Jedis 实例时是线程不安全的,因为它们共享一个socket变量,导致线程不安全。因此,推荐使用的方式是使用Jedis pool来线程安全的操作Redis。

  • Lettuce是线程安全的,它是基于Netty的,性能比较好。多线程使用同一连接实例时,也是线程安全的。

  • Springboot使用Redistemplate操作Redis,一旦配置后,RedisTemplate是线程安全的,能在多线程间重复利用,因为内部使用的也是使用连接池的方式。

  • Jedis的配置方式如下:

    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=your password
    spring.redis.timeout=2000
    spring.redis.jedis.pool.max-active=10
    spring.redis.jedis.pool.max-idle=10
    spring.redis.jedis.pool.max-wait=3
    spring.redis.database=0

    Lettuce的配置方式如下:

    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=your password
    spring.redis.lettuce.pool.max-active=10
    spring.redis.lettuce.pool.max-idle=10
    spring.redis.lettuce.pool.max-wait=3
    spring.redis.database=0

    统一的接口访问方式

    在Springboot中可以使用Jedis或者Lettuce的API来访问Redis,但这种原生方式比较麻烦,而RedisTemplate是SpringDataRedis中对Jedis或Lettuce API的高度封装,SpringDataRedis相对于原生API来说可以方便地更换Redis的Java客户端,比Jedis多了自动管理连接池的特性,方便与其他Spring框架进行搭配使用如:SpringCache。使用Redistemplate只需要在POM中增加下面的依赖。

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

    关于Redistemplate的API相比Jedis的API更加丰富,主要有以下两类。针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口

    ValueOperations:简单K-V操作
    SetOperations:set类型数据操作
    ZSetOperations:zset类型数据操作
    HashOperations:针对map类型的数据操作ListOperations:针对list类型的数据操作

    提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations:

    BoundValueOperations
    BoundSetOperations
    BoundListOperations
    BoundSetOperationsBoundHashOperations

    下面是一个创建Redistemplate的Bean实例。

     @Bean
     @Primary
     public RedisTemplate<String, Object> redisJacksonSerializerTemplate() {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(jedisConnectionFactory());
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new StringRedisSerializer());
            template.setHashKeySerializer(new JdkSerializationRedisSerializer());
            template.setHashValueSerializer(new JdkSerializationRedisSerializer());
            return template;  }

    丰富的序列化方式

    RedisTemplate提供了丰富的数据序列化和反序列化,使得操作更加简单,也更加优雅。Redis提供的接口比较单一,是byte[]和String,因此在使用这些API时,只能获取到数据后再在代码中增加反序列化操作,代码比较繁琐,而RedisTemplate除了字符串,byte数组,还提供了对象序列化,因此代码更加简洁。RedisTemplate为大多数操作提供了一个基于Java的序列化转换器。这意味着使用RedisTemplate写或读的任何对象都将会通过Java进行序列化/反序列化。RedisTemplate的序列化机制可以轻松的改变,Redis模块提供了多个可用的序列化实现,位于org.springframework.data.redis.serializer目录下。你也可以不设置序列化转换器,而使用原生的字节数组,同时需要设置enableDefaultSerializer为false。

    注意事项:如果使用你的数据需要被第三方工具解析,那么数据应该使用StringRedisSerializer而不是其他序列化方式。因为第三方工具无法简单的读取RedisTemplate序列化后的对象,后面有详细的演示每种序列化后的结果。

    JdkSerializationRedisSerializer

    适合于POJO对象的存取场景,POJO对象必须实现Serializable接口,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。序列化的结果是以xAC\xED\x00\x05t\x00开头,后面再接一个字符串长度(十六进制)+ key。但对于这种序列化后的数据,通过jedis很难读取到,只能使用scan方式或者硬编码前缀方式来读取。序列化后的截图如下:

    序列化设置如下:

    @Bean
    public RedisTemplate<String, Object> redisJacksonSerializerTemplate() {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(jedisConnectionFactory());

            template.setHashKeySerializer(new JdkSerializationRedisSerializer());
            template.setHashValueSerializer(new JdkSerializationRedisSerializer());
            return template;
    }

    StringRedisSerializer

    Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装,是最轻量级和高效的策略。当要存取的数据就是字符串类型数据的时候,那么使用StringRedisSerializer。如果是对象需要存储成json字符串,需要首先转换成字符串存储,或者使用JacksonJsonRedisSerializer,序列化后的结果如下图:当用第三方工具不是使用RedisTemplate来读取序列化后的数据时,比如jedis api来读取,此时最好使用StringRedisSerializer来序列化数据,因为Jedis只提供了byte[]和String两种读取方式,也没有提供自动反序列化设置,只能通过读取后的数据,自行反序列化。

    JacksonJsonRedisSerializer

    jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。指定enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)的话,存储到redis里的数据将是有类型的json数据,例如:

    ["java.util.ArrayList",[{"@class":"com.example.dto","id":12,"firstName":"tom","lastName":"zhang","type":2}]]

    @Bean
    @Primary
    public RedisTemplate<String, Object> redisJacksonSerializerTemplate() {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(jedisConnectionFactory());
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(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.WRAPPER_ARRAY);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            template.setHashKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            return template;
        }

    在activateDefaultTyping中需要设置DefaultTyping类型,DefaultTyping定义如下:

    public static enum DefaultTyping {
        JAVA_LANG_OBJECT,
        OBJECT_AND_NON_CONCRETE,
        NON_CONCRETE_AND_ARRAYS,
        NON_FINAL,
        EVERYTHING;
        private DefaultTyping() {
        }
    }

    JAVA_LANG_OBJECT: 当对象属性类型为Object时生效;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class User {
        private int age;
        private String name;
        private Object object;    
    }
    class l1nk3r {
        public int length = 100;
    }

    序列化后的结果如下:

    {"age":40,"name":"tom","object":["com.example.dto.l1nk3r",{"length":"100"}]}

    OBJECT_AND_NON_CONCRETE: 当对象属性类型为Object或者非具体类型(抽象类和接口)时生效;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class User {
        private int age;
        private String name;
        private Object object;    
        private Address address
    }
    class l1nk3r {
        public int length = 100;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class Address{
        private no;}

    序列化后的结果如下:

    {"age":40,"name":"tom","object":["com.example.dto.l1nk3r",{"length":"100"}],"address":["com.example.dto.Address",{"no":"No.1"}]}

    NON_CONCRETE_AND_ARRAYS: 同上, 另外所有的数组元素的类型都是非具体类型或者对象类型;

    {"age":40,"name":"tom","object":["com.example.dto.l1nk3r",[{"length":100},{"length":200}]],"address":["com.example.dto.Address",{"no":"No.2}]}

    NON_FINAL: 包括上文提到的所有特征,而且包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的的属性信息都需要被序列化和反序列化。

    ["com.example.dto.User",{"age":40,"name":"tom","object":["com.example.dto.l1nk3r",[{"length":100},{"length":200}]],"address":["com.example.dto.Address",{"no":"No.2}]}]

    ©著作权归作者所有,转载或内容合作请联系作者
    【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
    平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

    友情链接更多精彩内容