SpringDataRedis踩坑记录

这几天作的功能涉及到Redis缓存,踩了很多坑,这里记录下来。html

一、SpringBoot自动配置的RedisTemplate

在SpringBoot中能够在properties配置文件中配置spring.redis.*相关属性,SpringBoot就会自动帮你建立相关Redis链接以及RedisTemplate相关对象。java

@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
    
    // Redis链接的自动配置
  @Configuration
  @ConditionalOnClass(GenericObjectPool.class)
  protected static class RedisConnectionConfiguration { ... }
    
    
  /** * RedisTemplate相关配置,SpringBoot会为咱们生成两个RedisTemplate */
  @Configuration
  protected static class RedisConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
        RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
      RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
        RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
    }
  }
}

大多数状况下使用SpringBoot默认的配置便可。git

二、StringRedisTemplate与RedisTemplate

SpringBoot默认为咱们配置了两个RedisTemplate,其中StringRedisTemplate继承自RedisTemplate<String,String>github

public class StringRedisTemplate extends RedisTemplate<String, String> {
  public StringRedisTemplate() {
        // StringRedisTemplate默认使用StringRedisSerializer进行序列化
    RedisSerializer<String> stringSerializer = new StringRedisSerializer();
    setKeySerializer(stringSerializer);
    setValueSerializer(stringSerializer);
    setHashKeySerializer(stringSerializer);
    setHashValueSerializer(stringSerializer);
  }
  public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
    this();
    setConnectionFactory(connectionFactory);
    afterPropertiesSet();
  }
  protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
    return new DefaultStringRedisConnection(connection);
  }
}

二者的区别:web

一、StringRedisTemplate使用StringRedisSerializer进行序列化,而RedisTemplate默认使用JdkSerializationRedisSerializer进行序列化。redis

二、StringRedisTemplate对RedisConnection进行了一层包装。主要是由于RedisConnection的全部操做都是基于字节数组的,DefaultStringRedisConnection会把全部的结果转成String,包装了StringRedisSerializer并对批量操做数据进行批量序列化和反序列化,具体能够参考SetConverter,ListConverter,MapConverter的实现。spring

Spring Data Redis为了适配各类Redis客户端实现,抽象了一个RedisConnection接口。json

RedisConnection

事实上若是直接使用Jedis客户端,其实更方便,Jedis已经对String类型作了编解码处理。api

Jedis

package redis.clients.jedis;
public class Client extends BinaryClient implements Commands {
  ...
  public void hset(final String key, final String field, final String value) {
    hset(SafeEncoder.encode(key), SafeEncoder.encode(field), SafeEncoder.encode(value));
  }

  public void hget(final String key, final String field) {
    hget(SafeEncoder.encode(key), SafeEncoder.encode(field));
  }
  ...
}
//
package redis.clients.util;
public final class SafeEncoder {
  private SafeEncoder(){
    throw new InstantiationError( "Must not instantiate this class" );
  }

  public static byte[][] encodeMany(final String... strs) {
    byte[][] many = new byte[strs.length][];
    for (int i = 0; i < strs.length; i++) {
      many[i] = encode(strs[i]);
    }
    return many;
  }

  public static byte[] encode(final String str) {
    try {
      if (str == null) {
        throw new JedisDataException("value sent to redis cannot be null");
      }
      return str.getBytes(Protocol.CHARSET);
    } catch (UnsupportedEncodingException e) {
      throw new JedisException(e);
    }
  }

  public static String encode(final byte[] data) {
    try {
      return new String(data, Protocol.CHARSET);
    } catch (UnsupportedEncodingException e) {
      throw new JedisException(e);
    }
  }
}

三、使用RedisTemplate

使用RedisTemplate很简单,由于SpringBoot已经为咱们建立了RedisTemplate和StringRedisTemplate,因此咱们直接在须要使用的Bean里面注入就行:数组

@Component
public class Example {

    // 由于StringRedisTemplate继承自RedisTemplate<String, String>
    // 那么问题来了:
    // 这个地方注入的是StringRedisTemplate仍是普通的RedisTemplate呢
    @Autowired
    private RedisTemplate<String, String> template;

    @PostConstruct
    public void init() {
        // 答案是:StringRedisTemplate
        System.out.println(template.getClass());
    }
    
    // 只有明确声明RedisTemplate<Object, Object>才会注入普通的RedisTemplate
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

}

四、使用RedisTemplate的操做视图

RedisTemplate按照Redis的命令分组为咱们提供了相应的操做视图:

RedisTemplate操做视图

@Component
public class Example {

    @Autowired
    private StringRedisTemplate template;

    public void doSomething() {
        template.opsForList().leftPush("my-list", "value");
        template.opsForSet().add("my-set", "member1", "member2");
        ...
        BoundHashOperations<String, Object, Object> hashOps = template.boundHashOps("my-hash");
        hashOps.put("name", "holmofy");
        hashOps.put("age", "23");
        hashOps.put("gender", "male");
    }

}

这种随用随调方式的弊端是每次调用opsForXxx()都会建立一个新的视图。

SpringDataRedis能够直接注入视图

@Component
public class Example {
  
  // 只能用jsr250的@Resource注解注入
  @Resource(name="redisTemplate")
  private ListOperations<String, String> listOps;

  public void addLink(String userId, URL url) {
    listOps.leftPush(userId, url.toExternalForm());
  }
}

这个功能得益于PropertyEditorSupport,具体可参考该连接

五、RedisSerilizer

由于StringRedisTemplate和RedisTemplate默认使用的序列化不同,因此在使用视图操做时要注意一些序列化方面的细节:

@Component
public class Example {

    @Resource(name = "redisTemplate")
    private ValueOperations<String, Object> jdkSerializerValueOps;

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> stringSerializerValueOps;

    @PostConstruct
    public void doSomething() {
        jdkSerializerValueOps.set("jdkNumber", 1);
        jdkSerializerValueOps.set("jdkString", "1");
        stringSerializerValueOps.set("string", "1");

        try {
            jdkSerializerValueOps.increment("jdkNumber"); //失败
        } catch (Exception ignore) { }
        try {
            jdkSerializerValueOps.increment("jdkString"); //失败
        } catch (Exception ignore) { }
        try {
            stringSerializerValueOps.increment("string"); //成功
        } catch (Exception ignore) { }
    }
}

Redis

通过不一样的序列化器保存到Redis中的内容是不同的,StringRedisTemplate直接转成字符串保存到Redis里面,但RedisTemplate默认使用JdkSerializer会将对象信息存储到Redis中。

JdkSerializer优缺点

优势:序列化存储了类型信息,因此反序列化能直接生成相应对象。

缺点:

一、Redis中存储的内容包括对象头信息,存储了过多的无用内容,浪费Redis内存。

二、Redis中的一些操做不能使用,好比自增自减。

StringRedisSerializer优缺点

优势:

一、使用方便,全部的操做都以字符串形式保存到Redis

二、占用Redis更小

缺点:全部操做只能以字符串形式执行。StringRedisTemplate的key,value等参数都必须是String类型,由于StringRedisSerializer只负责把String转换成byte[]。存储对象时,须要咱们手动序列化成字符串;相应地,取对象须要反序列化。

六、其余序列化

目前最新的SpringDataRedis 2.1.5版默认提供了6种序列化方案。

RedisSerializer

GenericJackson2JsonRedisSerializer

底层使用Jackson进行序列化并存入Redis。对于普通类型(如数值类型,字符串)能够正常反序列化回相应对象。

但若是存入对象时因为没有存入类信息,则没法反序列化。

不过GenericJackson2JsonRedisSerializer默认为咱们开启了Jackson的类型信息的存储:

public GenericJackson2JsonRedisSerializer(String classPropertyTypeName) {

    this(new ObjectMapper());

    // 使用Jackson的类型功能嵌入反序列化所需的类型信息
    // the type hint embedded for deserialization using the default typing feature.
    mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));

    if (StringUtils.hasText(classPropertyTypeName)) {
        mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
    } else {
        mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
    }
}

因此当我存入一个对象时,它会把对象的类型信息也序列化存入Redis:

@Data
@AllArgsConstructor
@NoArgsConstructor
private class Person {

    private String name;
    private int age;

}

//{\"@class\":\"com.example.demo.Person\",\"name\":\"Tom\",\"age\":10}
jacksonSerializerValueOps.set("jsonObject", new Person("Tom", 10));
Object obj = jacksonSerializerValueOps.get("jsonObject");
System.out.println(obj.getClass()); // com.example.demo.Person

具体能够参考Jackson相关文档

Jackson2JsonRedisSerializer与GenericToStringSerializer

这两种序列化器是针对特定对象类型,前者用的是Jackson,后者用Spring的ConversionService。