Loading

RedisTemplate这玩意到底儿咋用啊

RedisTemplate中的几个角色:

  1. RedisSerializer:由于与Redis服务器的通信一定是使用字节数组完成的,所以RedisSerializer是将Java对象编码解码的组件
  2. RedisOperations:封装了一些Redis操作
  3. XXXOperations:封装了指定类型或功能的数据的操作,如ZSetOperations

RedisSerializer

RedisSerializer提供了两个方法,一个用于序列化,一个用于反序列化。并且,它提供了一个泛型T,代表该序列化器处理的类型。

public interface RedisSerializer<T> {

	@Nullable
	byte[] serialize(@Nullable T t) throws SerializationException;

	@Nullable
	T deserialize(@Nullable byte[] bytes) throws SerializationException;

}

它的实现类有下面这些:

img

从实现类的名字可以看出,其中有将对象转换为json的,有使用JDK自带的序列化机制进行序列化反序列化的,有专门处理String的...

默认情况下,RedisTemplate使用JdkSerializationRedisSerializer,也就是JDK默认的序列化机制来进行序列化

默认序列化器

RedisTemplate的成员属性中有如下和序列化器相关的属性:

// 是否启用默认序列化器
private boolean enableDefaultSerializer = true;
// 默认序列化器
private @Nullable RedisSerializer<?> defaultSerializer;

// 键的序列化器
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
// 值序列化器
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
// hash键序列化器
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
// hash值序列化器
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
// 字符串序列化器,使用StringRedisSerializer
private RedisSerializer<String> stringSerializer = RedisSerializer.string();

从这里我们可以看出,我们可以对RedisTemplate进行设置,在不同的情况下使用不同的序列化器,如在hash值的序列化上使用Jdk序列化器,而在普通的值上使用字符串序列化器。

RedisTemplate继承了InitailizingBean,所以,在Bean的初始化阶段结束后,它的afterPropertiesSet方法被调用并执行了以下代码:

@Override
public void afterPropertiesSet() {
    super.afterPropertiesSet();
    boolean defaultUsed = false;
    // 如果默认初始化器为空,创建JdkSerializationRedisSerializer
    if (defaultSerializer == null) {
        defaultSerializer = new JdkSerializationRedisSerializer(
                classLoader != null ? classLoader : this.getClass().getClassLoader());
    }

    // 如果 enableDefaultSerializer = true
    // 将key/value/hashkey和hashvalue的序列化器都设置成默认序列化器
    if (enableDefaultSerializer) {
        if (keySerializer == null) {
            keySerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (valueSerializer == null) {
            valueSerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (hashKeySerializer == null) {
            hashKeySerializer = defaultSerializer;
            defaultUsed = true;
        }
        if (hashValueSerializer == null) {
            hashValueSerializer = defaultSerializer;
            defaultUsed = true;
        }
    }

    if (enableDefaultSerializer && defaultUsed) {
        Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
    }

    if (scriptExecutor == null) {
        this.scriptExecutor = new DefaultScriptExecutor<>(this);
    }

    initialized = true;
}

所以默认情况下,所有的(除了那个stringSerializer之外)操作都将使用Jdk自带的序列化和反序列化来对对象进行编解码,这要求你传入到Redis中的对象必须实现了Serializable接口

下面是默认情况下的一个示例:

@SpringBootTest
public class RedisTemplateTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void test() {
        redisTemplate.opsForValue().set("key", "value");
    }

}

去Redis中查询,出现了莫名其妙的前缀,这是Java序列化机制添加的。

img

序列化器的设置

在下面的例子中,我们将默认序列化器设置成了StringRedisSerializer,所以,在我们插入字符串时,它会按照字符串的方式编码

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

再次运行之前的测试用例,redis中出现了这个结果:

img

StringRedisSerializer只支持字符串类型的转化,而且默认使用UTF-8编码,所以现在,如果你使用非String的对象,应该会出错

@Test
void testObject() {
    Student student = new Student();
    redisTemplate.opsForValue().set("student", student);
}

img

而在使用最初的Jdk序列化器时,则不会出现这个错误,下面,我们将键的序列化器设置成String的,将值的设置成Jdk序列化器,这样,我们可以保证在key上不会出现那些奇奇怪怪的前缀字符,而且可以将值设置成对象

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate redisTemplate = new RedisTemplate();
    redisTemplate.setConnectionFactory(factory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    // 默认情况下不设置也是这个
    redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
    redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
    return redisTemplate;
}

img

StringRedisTemplate

StringRedisTemplate的实现如下,即将这四个序列化器都设置成String序列化器

public class StringRedisTemplate extends RedisTemplate<String, String> {

	public StringRedisTemplate() {
		setKeySerializer(RedisSerializer.string());
		setValueSerializer(RedisSerializer.string());
		setHashKeySerializer(RedisSerializer.string());
		setHashValueSerializer(RedisSerializer.string());
	}

}

将对象设置到Redis中

下午的时候,一个公司的面试官打了我的电话,在电话里进行了一波猝不及防的面试。其中有一个我从未思考过的问题——如何将对象加入到Redis中

我当时能想到的就是将对象转换成字符串或者一个一个属性的设置到hash中,之后我也上网查了一下,网上流传着三种将对象设置到Redis中的方式:

  1. 序列化成字节数组,以字符串的方式存到任何想存的地方
  2. 转换成Json,以Json字符串的方式存储
  3. 将属性一个一个的设置到Hash中

下面演示使用RedisTemplate实现这三种插入对象的方式

测试类

下面是测试类的外壳,创建了一个Student对象用于测试,从外部注入一个RedisTemplate。

@SpringBootTest
public class SaveObjectTest {
    @Autowired
    private RedisTemplate redisTemplate;

    Student student = new Student("10001", "Yudoge");

}

Student类实现了Serializable接口和equals方法,equals方法通过比较内部两个字段的值来判断两个对象是否相等

序列化成字节数组

这个在RedisTemplate中很简单,默认的Jdk序列化器就提供了这种方式。

@Test
void testSaveObjectByByteArray() {
    // 插入到数据库
    redisTemplate.opsForValue().set("student", student);
    // 从数据库中拿
    Student studentFromDB = (Student) redisTemplate.opsForValue().get("student");

    // 判断相等
    Assertions.assertEquals(student, studentFromDB);
}

序列化成json字符串

当我们需要序列化成json字符串时,默认的RedisTemplate就满足不了我们了,我们得自己定义一个新的RedisTemplate

@Bean
@Qualifier("valueToJsonTemplate")
public RedisTemplate jsonTemplate(RedisConnectionFactory factory) {
    RedisTemplate redisTemplate = new RedisTemplate();
    redisTemplate.setConnectionFactory(factory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    // 使用Json转换的Serializer
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
    return redisTemplate;
}

注意,使用该Serializer需要导入jackson-databind的包

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

然后,编写测试:

@Autowired
@Qualifier("valueToJsonTemplate")
private RedisTemplate valueToJsonTemplate;

@Test
void testSaveObjectByJsonString() {
    valueToJsonTemplate.opsForValue().set("studentJson", student);
    Student studentFromDB = (Student) valueToJsonTemplate.opsForValue().get("studentJson");

    Assertions.assertEquals(student, studentFromDB);
}

数据库中的值:

img

如果觉得这样写不够优雅,我们也可以学StringRedisTemplate,来编写一个JsonRedisTemplate

@Component
public class JsonRedisTemplate extends RedisTemplate<String, Object> {
    @Autowired
    public JsonRedisTemplate(RedisConnectionFactory connectionFactory) {
        setKeySerializer(RedisSerializer.string());
        setValueSerializer(RedisSerializer.json());
        setHashKeySerializer(RedisSerializer.string());
        setHashValueSerializer(RedisSerializer.json());
        setConnectionFactory(connectionFactory);
        afterPropertiesSet();
    }

    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        return new DefaultStringRedisConnection(connection);
    }
}

然后代码中使用JsonRedisTemplate,这样能通过类型清楚的看出当前使用的RedisTemplate会将对象转换成Json。

@Autowired
private JsonRedisTemplate valueToJsonTemplate;

@Test
void testSaveObjectByJsonString() {
    valueToJsonTemplate.opsForValue().set("studentJson", student);
    Student studentFromDB = (Student) valueToJsonTemplate.opsForValue().get("studentJson");

    Assertions.assertEquals(student, studentFromDB);
}

将属性一个个设置成hash中的值

@Test
void testSaveObjectToHash() {
    String key = "studentHash";
    HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();
    hashOperations.put(key, "id", student.getId());
    hashOperations.put(key, "name", student.getName());

    Student studentFromDB = new Student(
            hashOperations.get(key, "id"),
            hashOperations.get(key, "name")
    );

    Assertions.assertEquals(student, studentFromDB);
}

img

如果你觉得自己处理这种转换并不优雅,那么你可以提供一个基于反射的工具类。

思考

我记得面试官说,如果你把一个对象直接存到Redis中,这个对象如果不实现hashCode可能会出一些问题。

我想,唯一的问题可能就是,如果你不实现hashCode,那么在应用层面上,你从数据库中取到的对象和你设进去时候的原始对象的哈希值不同,会被Java中的Hash容器认为不是一个对象。这样的话,equals也有同样的问题。

我想这不是面试官的意思,但我也只能理解到这了~~

posted @ 2022-08-04 20:11  yudoge  阅读(245)  评论(0编辑  收藏  举报