RedisTemplate是Spring Data Redis提供给用户的最高级的抽象客户端,用户可直接通过RedisTemplate对Redis进行多种操作。
在项目中使用需要引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
因为redis是以key-value的形式将数据存在内存中,key就是简单的string,key似乎没有长度限制,不过原则上应该尽可能的短小且可读性强,无论是否基于持久存储,key在服务的整个生命周期中都会在内存中,因此减小key的尺寸可以有效的节约内存,同时也能优化key检索的效率。
value在redis中,存储层面仍然基于string,在逻辑层面,可以是string/set/list/map,不过redis为了性能考虑,使用不同的“encoding”数据结构类型来表示它们。(例如:linkedlist,ziplist等)。
所以可以理解为,其实redis在存储数据时,都把数据转化成了byte[]数组的形式,那么在存取数据时,需要将数据格式进行转化,那么就要用到序列化和反序列化了,这也就是为什么需要配置Serializer的原因。
尽管Redis本身支持多种数据类型,但是Redis底层还是使用二进制存储数据,所以我们需要在应用层对数据的格式进行转换,这种转换称之为序列化。
针对二进制数据的“序列化/反序列化”,Spring提供了一个顶层接口RedisSerializer,并提供了多种实现可供选择(RedisSerializer),如下所示:
下面四个是Spring自带的:
- StringRedisSerializer
- JdkSerializationRedisSerializer
- Jackson2JsonRedisSerializer
- GenericJackson2JsonRedisSerializer
- Jackson2JsonRedisSerializer
如果项目中引入了fastjson:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
还会看到下面2个fastjson的实现类:
- FastJsonRedisSerializer
- GenericFastJsonRedisSerializer
StringRedisSerializer
StringRedisSerializer的使用如下:
package com.morris.spring.boot.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* test StringRedisSerializer
*/
@Slf4j
public class StringRedisSerializerDemo {
public static void main(String[] args) {
StringRedisSerializer serializer = new StringRedisSerializer();
String s = "hello";
byte[] bytes = serializer.serialize(s);
log.info("{} => {}", s, bytes);
String str = serializer.deserialize(bytes);
log.info("{} => {}", bytes, str);
}
}
运行结果如下:
19:36:49.744 [main] INFO com.morris.spring.boot.redis.StringRedisSerializerDemo - hello => [104, 101, 108, 108, 111]
19:36:49.747 [main] INFO com.morris.spring.boot.redis.StringRedisSerializerDemo - [104, 101, 108, 108, 111] => hello
StringRedisSerializer只是对简单的字符串序列化,可读性好,可用于key的序列化。内部就是通过String类的new String(bytes) & string.getBytes()实现的序列化,源码如下:
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(@Nullable String string) {
return (string == null ? null : string.getBytes(charset));
}
JdkSerializationRedisSerializer
JdkSerializationRedisSerializer的使用如下:
package com.morris.spring.boot.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
/**
* test JdkSerializationRedisSerializer
*/
@Slf4j
public class JdkSerializationRedisSerializerDemo {
public static void main(String[] args) {
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
String s = "hello";
byte[] bytes = serializer.serialize(s);
log.info("{} => {}", s, new String(bytes));
Object str = serializer.deserialize(bytes);
log.info("{} => {}", new String(bytes), str);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
log.info("{} => {}", new String(userBytes), u);
}
}
运行结果如下:
19:51:11.240 [main] INFO com.morris.spring.boot.redis.JdkSerializationRedisSerializerDemo - hello => �� t hello
19:51:11.290 [main] INFO com.morris.spring.boot.redis.JdkSerializationRedisSerializerDemo - �� t hello => hello
19:51:11.299 [main] INFO com.morris.spring.boot.redis.JdkSerializationRedisSerializerDemo - [196]User(name=morris, age=18) => �� sr !com.morris.spring.boot.redis.User����}1)9 L aget Ljava/lang/Integer;L namet Ljava/lang/String;xpsr java.lang.Integer⠤���8 I valuexr java.lang.Number��?��? xp t morris
19:51:11.300 [main] INFO com.morris.spring.boot.redis.JdkSerializationRedisSerializerDemo - �� sr !com.morris.spring.boot.redis.User����}1)9 L aget Ljava/lang/Integer;L namet Ljava/lang/String;xpsr java.lang.Integer⠤���8 I valuexr java.lang.Number��?��? xp t morris => User(name=morris, age=18)
JdkSerializationRedisSerializer直接使用Java提供的序列化方式,效率高,占用空间少,可读行差,被序列化的对象必须实现Serializable接口,否则会抛出异常。
JdkSerializationRedisSerializer序列化底层使用的是ObjectOutputStream:
public void serialize(Object object, OutputStream outputStream) throws IOException {
if (!(object instanceof Serializable)) {
throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
"but received an object of type [" + object.getClass().getName() + "]");
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
}
JdkSerializationRedisSerializer反序列化底层使用的是ObjectInputStream:
public Object deserialize(InputStream inputStream) throws IOException {
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
try {
return objectInputStream.readObject();
}
catch (ClassNotFoundException ex) {
throw new NestedIOException("Failed to deserialize object type", ex);
}
}
Jackson2JsonRedisSerializer
public static void errorTest() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [26]User(name=morris, age=18) => {"name":"morris","age":18}
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// java.util.LinkedHashMap cannot be cast to com.morris.spring.boot.redis.User
log.info("{} => {}", new String(userBytes), u);
}
运行结果如下:
09:41:48.251 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - [26]User(name=morris, age=18) => {"name":"morris","age":18}
3 actionable tasks: 2 executed, 1 up-to-date
Exception in thread "main" java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.morris.spring.boot.redis.User
at com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo.errorTest(Jackson2JsonRedisSerializerDemo.java:34)
at com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo.main(Jackson2JsonRedisSerializerDemo.java:15)
直接使用Jackson2JsonRedisSerializer序列化Object任意对象时没问题,但是在进行反序列化时会抛出java.util.LinkedHashMap cannot be cast to XXX
异常。
Jackson2JsonRedisSerializer创建时如果不使用Object,而是指定具体的对象User就不会抛出异常,序列化出来的json也不包含额外的信息。
public static void testNotObject() {
Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<>(User.class);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [26]User(name=morris, age=18) => {"name":"morris","age":18}
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = serializer.deserialize(userBytes);
// {"name":"morris","age":18} => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
允许结果如下:
09:45:07.795 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - [26]User(name=morris, age=18) => {"name":"morris","age":18}
09:45:07.833 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - {"name":"morris","age":18} => User(name=morris, age=18)
这样做的缺点就是对不同的对象进行序列化时要创建不同的Jackson2JsonRedisSerializer,非常麻烦。
那么有没有一种方法能实现反序列化任意对象不抛出上面的异常呢?可以通过设置ObjectMapper的一个属性来解决。
public static void testEnableDefaultTyping() {
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [64]User(name=morris, age=18) => ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}]
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}] => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
运行结果如下:
09:50:55.843 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - [64]User(name=morris, age=18) => ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}]
09:50:55.894 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}] => User(name=morris, age=18)
可以看到对象序列化后会将class的全限定名写入到结果中,这样在反序列化时就能知道需要将数据反序列化成什么对象了。
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
的含义就是对于除了一些原生类型(String、Double、Integer、Double)类型外的非常量(non-final)类型,类型将会序列化在结果上,以便可以在JSON反序列化正确的推测出值所属的类型。
上面的api已经过时了,可以使用下面的新api:
public static void testNewApi() {
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator() ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
serializer.setObjectMapper(objectMapper);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// PROPERTY {"@class":"com.morris.spring.boot.redis.User","name":"morris","age":18} => User(name=morris, age=18)
// WRAPPER_ARRAY ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}] => User(name=morris, age=18)
// WRAPPER_OBJECT {"com.morris.spring.boot.redis.User":{"name":"morris","age":18}} => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
运行结果如下:
09:55:32.821 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - [64]User(name=morris, age=18) => ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}]
09:55:32.878 [main] INFO com.morris.spring.boot.redis.Jackson2JsonRedisSerializerDemo - ["com.morris.spring.boot.redis.User",{"name":"morris","age":18}] => User(name=morris, age=18)
GenericJackson2JsonRedisSerializer
package com.morris.spring.boot.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
/**
* test GenericJackson2JsonRedisSerializer
*/
@Slf4j
public class GenericJackson2JsonRedisSerializerDemo {
public static void main(String[] args) {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [71]User(name=morris, age=18) => {"@class":"com.morris.spring.boot.redis.User","name":"morris","age":18}
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// {"@class":"com.morris.spring.boot.redis.User","name":"morris","age":18} => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
}
运行结果如下:
10:01:37.428 [main] INFO com.morris.spring.boot.redis.GenericJackson2JsonRedisSerializerDemo - [71]User(name=morris, age=18) => {"@class":"com.morris.spring.boot.redis.User","name":"morris","age":18}
10:01:37.485 [main] INFO com.morris.spring.boot.redis.GenericJackson2JsonRedisSerializerDemo - {"@class":"com.morris.spring.boot.redis.User","name":"morris","age":18} => User(name=morris, age=18)
从运行结果可以看出GenericJackson2JsonRedisSerializer确实如他的名字那样天生就支持泛型,他也是通过将类信息序列化到结果中实现的。
从GenericJackson2JsonRedisSerializer的构造方法源码中也可以看出是通过设置ObjectMapper的属性来实现,跟我们上面手动指定的配置一样。
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(new ObjectMapper());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
registerNullValueSerializer(mapper, classPropertyTypeName);
if (StringUtils.hasText(classPropertyTypeName)) {
mapper.activateDefaultTypingAsProperty(mapper.getPolymorphicTypeValidator(), DefaultTyping.NON_FINAL,
classPropertyTypeName);
} else {
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.NON_FINAL, As.PROPERTY);
}
}
FastJsonRedisSerializer
public static void testNotObject() {
FastJsonRedisSerializer<User> serializer = new FastJsonRedisSerializer<>(User.class);
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [26]User(name=morris, age=18) => {"age":18,"name":"morris"}
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// {"age":18,"name":"morris"} => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
运行结果如下:
11:27:52.926 [main] INFO com.morris.spring.boot.redis.FastJsonRedisSerializerDemo - [26]User(name=morris, age=18) => {"age":18,"name":"morris"}
11:27:52.950 [main] INFO com.morris.spring.boot.redis.FastJsonRedisSerializerDemo - {"age":18,"name":"morris"} => User(name=morris, age=18)
FastJsonRedisSerializer与Jackson2JsonRedisSerializer一样不支持泛型,需要支持泛型请使用GenericFastJsonRedisSerializer。
GenericFastJsonRedisSerializer
package com.morris.spring.boot.redis;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import lombok.extern.slf4j.Slf4j;
/**
* test GenericFastJsonRedisSerializer
*/
@Slf4j
public class GenericFastJsonRedisSerializerDemo {
public static void main(String[] args) {
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
User user = new User("morris", 18);
byte[] userBytes = serializer.serialize(user);
// [70]User(name=morris, age=18) => {"@type":"com.morris.spring.boot.redis.User","age":18,"name":"morris"}
log.info("[{}]{} => {}", userBytes.length, user, new String(userBytes));
User u = (User) serializer.deserialize(userBytes);
// {"@type":"com.morris.spring.boot.redis.User","age":18,"name":"morris"} => User(name=morris, age=18)
log.info("{} => {}", new String(userBytes), u);
}
}
运行结果如下:
11:29:09.886 [main] INFO com.morris.spring.boot.redis.GenericFastJsonRedisSerializerDemo - [70]User(name=morris, age=18) => {"@type":"com.morris.spring.boot.redis.User","age":18,"name":"morris"}
11:29:09.915 [main] INFO com.morris.spring.boot.redis.GenericFastJsonRedisSerializerDemo - {"@type":"com.morris.spring.boot.redis.User","age":18,"name":"morris"} => User(name=morris, age=18)
GenericFastJsonRedisSerializer与GenericJackson2JsonRedisSerializer一样会将类信息序列化到结果中。