Redis客户端中文乱码

网上非常常见的乱码,百度一搜大部分都是这种情况。如果不是此种情况请看下一节。

redis-cli
redis 127.0.0.1:6379> set 'name' '中文'
OK
redis 127.0.0.1:6379> get 'name'
"\xd6\xd0\xce\xc4"
redis 127.0.0.1:6379>

客户端查看乱码,这个情况我们只要将修改客户端命令行就可以。redis-cli --raw

redis-cli --raw
redis 127.0.0.1:6379> get 'name'
中文
redis 127.0.0.1:6379>

Jedis 或 RedisTemplate获取乱码

  • 直接使用Jedis 或 RedisTemplate实现存取没有乱码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, 
	webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

//    @Autowired
//    StringRedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate(){
        ValueOperations<String, String> valueOperation = 
	        redisTemplate.opsForValue();
        valueOperation.set("testname, "NO爷");
        String testName = valueOperation.get("testname");
        System.out.println(testName);//输出 NO爷
    }

    @Test
    public void testJedis(){
        Jedis jedis = new Jedis("192.168.130.247");
        jedis.set("nickname", "NO爷");
        String nickName = jedis.get("nickname");
        System.out.println(nickName);//输出 NO爷
    }
}
  • 客户端直接存储,使用Jedis或者Redis读取数据乱码,这个地方直接使用程序直接读取上一节Redis客户端存储的“name”,这个时候获取到乱码“����”。
    这种情况是由于redis安装环境和Java程序环境的编码格式不一致导致。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, 
	webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest2 {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

//    @Autowired
//    StringRedisTemplate redisTemplate;

	@Test
	public void testRedisTemplate(){
	    ValueOperations<String, String> valueOperation = 
		    redisTemplate.opsForValue();
	    String testName = valueOperation.get("name");
	    System.out.println(testName);//输出 ����,不同地环境可能对其他乱码。
	}
	
	@Test
	public void testJedis(){
	    Jedis jedis = new Jedis("192.168.130.247");
	    String nickName = jedis.get("name");
	    System.out.println(nickName);//输出 ����,不同地环境可能对其他乱码。
	}
}
  • 针对乱码解决:改变获取数据方式,不要直接获取字符串,而是获取字节数组。
    还有一种更加优雅的方式见下一节的源码分析。
    直接获取字符串会直接破环原始字节数据,如果是原始数据格式ISO-8859-1,我们可以直接使用new String(name.getBytes(“ISO-8859-1”), “UTF-8”),这种方式解析。但是如果是GBK等其他编码格式就会搞出其他乱码,这个具体我也不清楚了。
    我的Redis服务器编码格式GBK,Java运行环境是UTF-8。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, 
	webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest3 {

    @Autowired
    RedisTemplate<String, String> redisTemplate;

//    @Autowired
//    StringRedisTemplate redisTemplate;

	@Test
    public void testRedisTemplate() throws UnsupportedEncodingException {
        RedisConnection connection = 
	        redisTemplate.getConnectionFactory().getConnection();
        byte[] name = connection.get("name".getBytes());
        System.out.println(new String(name, "GBK"));// 输出 中文
    }

    @Test
    public void testJedis() throws UnsupportedEncodingException {
        Jedis jedis = new Jedis("192.168.130.247");
        byte[] name = jedis.get("name".getBytes());
        System.out.println(new String(name, "GBK"));// 输出 中文
    }
}

源码分析

  • 上文列举了Jedis和RedisTemplate两种方式,因为RedisTemplate的底层实现就是Jedis。
  • 上文使用RedisTemplate模板类是使用RedisTemplate注入,这个地方可以直接使用StringRedisTemplate注入。
    使用RedisTemplate注入需要指定泛型类型,需要指定序列化对象,否则使用默认序列化机制,这个更加容易出现乱码。
/**
 * RedisTemplate 代码片段
 * 可以看到默认序列化类是使用JdkSerializationRedisSerializer,
 * 这个也会导致程序读取乱码。
 */
public void afterPropertiesSet() {
    super.afterPropertiesSet();
    boolean defaultUsed = false;
    if (this.defaultSerializer == null) {
        this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
    }

    if (this.enableDefaultSerializer) {
        if (this.keySerializer == null) {
            this.keySerializer = this.defaultSerializer;
            defaultUsed = true;
        }

        if (this.valueSerializer == null) {
            this.valueSerializer = this.defaultSerializer;
            defaultUsed = true;
        }

        if (this.hashKeySerializer == null) {
            this.hashKeySerializer = this.defaultSerializer;
            defaultUsed = true;
        }

        if (this.hashValueSerializer == null) {
            this.hashValueSerializer = this.defaultSerializer;
            defaultUsed = true;
        }
    }

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

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

    this.initialized = true;
}
/**
 * StringRedisTemplate 源码
 * 在初始构造时使用了StringRedisSerializer类来实现序列化,
 * 一般Redis如果存储的数据没有特殊的话都是使用这个。
 */
public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        this.setKeySerializer(stringSerializer);
        this.setValueSerializer(stringSerializer);
        this.setHashKeySerializer(stringSerializer);
        this.setHashValueSerializer(stringSerializer);
    }

    public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
        this();
        this.setConnectionFactory(connectionFactory);
        this.afterPropertiesSet();
    }

    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        return new DefaultStringRedisConnection(connection);
    }
}
  • 常用的序列化还有json、xml等格式选择。
  • 我们再看一下StringRedisSerializer源码,在构造时直接指定编码格式为UTF-8,同时提供了构造重载设置编码格式。那么我们再试试,使用“GBK”编码的StringRedisSerializer是否还会有乱码?
/**
 * StringRedisSerializer 源码
 */
public class StringRedisSerializer implements RedisSerializer<String> {
    private final Charset charset;

    public StringRedisSerializer() {
        this(Charset.forName("UTF8"));
    }

    public StringRedisSerializer(Charset charset) {
        Assert.notNull(charset, "Charset must not be null!");
        this.charset = charset;
    }

    public String deserialize(byte[] bytes) {
        return bytes == null ? null : new String(bytes, this.charset);
    }

    public byte[] serialize(String string) {
        return string == null ? null : string.getBytes(this.charset);
    }
}
/**
 * 使用带有GBK编码的构造实例化StringRedisSerializer,
 * 正如所料,正确读取到中文数据。
 */
@Test
public void testRedisTemplate(){
    RedisSerializer<String> stringSerializer = new StringRedisSerializer(Charset.forName("GBK"));
    redisTemplate.setKeySerializer(stringSerializer);
    redisTemplate.setValueSerializer(stringSerializer);
    redisTemplate.setHashKeySerializer(stringSerializer);
    redisTemplate.setHashValueSerializer(stringSerializer);
    ValueOperations<String, String> valueOperation = redisTemplate.opsForValue();
    String name = valueOperation.get("name");
    System.out.println(name);//输出 中文
}