最近在做Redis缓存的时候,遇到了一个棘手的问题,简单来说就是项目使用Spring的RedisTemplate进行Redis数据存取操作,实际应用中发现Redis中key和value会出现“无意义”乱码前缀。如果是普通的java程序是没有这个问题。
本文解决Redis乱码问题,所以对Spring MVC集成Redis没有太多的涉及(这内容将在下一篇博客补齐),同时对序列化进行了些许探究。
问题产生
最近在Spring MVC用到Redis缓存的时候,写了一个Test文件对Redis数据进行了验证,可一直在后台传不进数据,相关代码如下:
关于redis如何集成在Spring MVC里,请看我下一篇博客内容,本文内容是解决序列化问题。
public class RedisTest extends BaseJunit4Test{
@Resource
private RedisTemplate redisTemplate;
@Test
public void testRedis() {
redisTemplate.opsForValue().set("name", "ayccc");
String name = (String) redisTemplate.opsForValue().get("name");
System.out.println("value of name is:" + name);
}
}
这个代码就是普通的向Redis写入和查询的工作,此时我在控制台打印的信息就是ayccc.
但是我去redis-cli查看的时候发现name的value竟然是nil,线外之意就是没有传到,但是为什么我此时的测试文件能打印出来呢?
首先测试了是否连接上Redis,通过日志分析,能够打开Redis,那么连接上是没有问题的。
那问题会不会出现在redis呢?
使用命令 keys *列举出我的ke
y值有哪些,这个方法非常有效的。
发现此时name前缀出现了这么多的编码,我立马就想到了这是编码问题(此时对Redis没有概念,不知道是序列化问题),更改项目的UTF-8值以后,发现依旧是这样的问题,这不禁让我重新思考。
我重新在测试文件又插入了一个数据,此时插入的key为htf:
发现这里出现的htf和name的前缀有点相似,前五个前缀都是一样的。
拿到这块编码很自然地去分析乱码问题:
\x对应0x
\xac\xed对应是0xaced,是ObjectOutputStream的序列化魔数(见java.io.ObjectStreamConstants.STREAM_MAGIC)。
\x00\x05对应是5,是ObjectOutputStream的序列化版本(见java.io.ObjectStreamConstants.STREAM_VERSION)。
这里引出一个小问题:为什么是\x00\x05而不是\x05?
因为上面2个值write时采用的是short,占2个字节。
样例乱码\x05后面有个t,不是很明显。t是转化后的ASCII码值对应字符,对应16进制是0x74,是ObjectOutputStream分配给String类型标记(见java.io.ObjectStreamConstants.TC_STRING)。
\x00-是有\x00和-组成的,是一起的,表示数据的字节数。-是转化后的ASCII码值对应字符,对应16进制是0x2d(10进制是45,样例abcd🔤xxxxxx:passport:associated🔑29708的字符数就是45,1个字符1个字节,字节数也是45)。
为什么是这样一串奇怪的 16 进制? ObjectOutputStream#writeString(String str, boolean unshared) 实际就是标志位 + 字符串长度 + 字符串内容
ok 问题找到了,意思就是redis的序列化问题,但什么是序列化,虽然有些了解但还是不多,索性又进行了探索。
序列化是什么?
序列化:把对象转化为可传输的字节序列过程称为序列化。序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。
因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。
一般来说支持序列化的有这些:JDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言)
下面通过java来简单展示一下序列化。
java 实现序列化很简单,只需要实现Serializable 接口即可。
import java.io.Serializable;
public class Employee implements Serializable{
private static final long serialVersionUID = 1L; //Serial Version UID
int id;
String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
}
这里提一句,Serializable接口是没有任何方法的:
public interface Serializable {
}
Serializable是一个所谓的标记接口,也就是说,实现这个接口是给这个类贴个标签,说它是Serializable的就可以了,具体实现是由JVM内部实现的,这个标签实际上是告诉JVM,你可以将我序列化。但这个标签不是随便贴的,如果你给一个类贴了这个标签,却在内部用到没贴这个标签的类,那运行时就可能有异常抛出。标记接口的用法现在一般被Annotation代替了,但Serializable是在Annotation还没出现前就存在了的。
序列化操作:
import java.io.*;
public class Persist{
public static void main(String args[]){
try{
Employee emp1 =new Employee(20110,"John");
Employee emp2 =new Employee(22110,"Jerry");
Employee emp3 =new Employee(20120,"Sam");
FileOutputStream fout=new FileOutputStream("output.txt");
ObjectOutputStream out=new ObjectOutputStream(fout);
out.writeObject(emp1);
out.writeObject(emp2);
out.writeObject(emp3);
out.flush();
out.close();
System.out.println("Serialization and Deserialization is been successfully executed");
}
catch(Exception e){
System.out.println(e);}
}
}
此时看一下output.txt文件,不出所料都是乱码:
此时已经将类的属性保存在了output.java中了.
反序列化:
import java.io.*;
public class Depersist{
public static void main(String args[]){
try{
ObjectInputStream in=new ObjectInputStream(new FileInputStream("output.txt"));
Employee e1=(Employee)in.readObject();
Employee e2=(Employee)in.readObject();
Employee e3=(Employee)in.readObject();
System.out.println(e1.id+" "+e1.name);
System.out.println(e2.id+" "+e2.name);
System.out.println(e3.id+" "+e3.name);
in.close();
}
catch(Exception e){
System.out.println(e);}
}
}
运行结果:
分析一下writeObject函数:
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
进入到writeObject0(obj, false)
最重要的部分源码:
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo)
注意String 实现了 Serializable 接口,表明如果上面的Employee没有实现此接口会进行报错。
解决乱码问题
现在问题找到了,解决问题就很简单了,问题就出在Spring MVC里面。
org.springframework.data.redis.core.RedisTemplate实例化需要序列化和反序列化组件,如果我们不指定,默认使用org.springframework.data.redis.serializer.JdkSerializationRedisSerializer进行序列化,而JdkSerializationRedisSerializer最终使用的是Java原生java.io.ObjectOutputStream.ObjectOutputStream(OutputStream)进行序列化。这个乱码前缀就是ObjectOutputStream进行序列化时添加的。
现在就需要改变:
一般有两种办法,一是使用配置文件二是写配置类,这里都将解决。
我一开始配置的工具是这个:说白了就是没有实现相应的序列化操作
spring-redis.xml:
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
p:connection-factory-ref="redisConnectionFactory">
</bean>
将上述xml改成这个就好:绝大多数情况下,不推荐使用 JdkSerializationRedisSerializer 进行序列化。主要是不方便人工排查数据。
<!--使用字符串进行序列化-->
<bean id="stringReadisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<!--使用JDK的序列化器进行转化-->
<bean id="jdkSerializationRedisSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
<!--配置Spring RedisTemplate-->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnectionFactory"/>
<property name="keySerializer" ref="stringReadisSerializer"/>
<property name="valueSerializer" ref="stringReadisSerializer"/>
</bean>
或者可以使用配置类:
package com.ay.conf;
import org.aspectj.lang.annotation.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
@Configuration
public class RedisConfig {
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
此配置类方法非常高效,将在后面对此进行深入研究。
以上就将问题全部解决了。