字符编码问题记录

需求&问题

须要对序列化之后的对象 (java中的byte[]) 在redis中进行存取
因为redis声称只支持String(做为redis暴露出来的最基本的数据类型)形式的存取 (ref: https://redis.io/topics/internals, https://redis.io/topics/internals )
因此须要在存取先后将byte[]与String互相转换java

发现从string decode出来的byte[]跟encode以前的byte[]不同
即便强制指定了一致的编码解码方式, 结果仍不符合预期redis

byte[] origin = eh.toBytes(event); // serialized event

String str1 = new String(origin);
byte[] new1 = str1.getBytes();
System.out.println(Arrays.equals(origin, new1));
// output: false

String str2 = new String(origin, StandardCharsets.US_ASCII);
byte[] new2 = str2.getBytes(StandardCharsets.US_ASCII);
System.out.println(Arrays.equals(origin, new2));
// output: false

String str3 = new String(origin, StandardCharsets.UTF_8);
byte[] new3 = str3.getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.equals(origin, new3));
// output: false

猜想&尝试

  1. 怀疑是系统的默认编码方式与解码时指定的不一样, 如上所示 强制指定后未果算法

  2. 照理说编码解码的算法是对称的, 对一个byte[]编码解码后的到byte[]理应也是同样的. 尝试使用apache的StringUtils编码解码, 结果徒然apache

缘由&解释

经搜索试验后发现缘由既与这个byte[]自己有关又与编码方式有关:数组

该场景中event结构中包含一个UUID, 未序列化前在java中以一个长度为32个字符的字符串表示, 例子“ce4326f3694b479dad472f250b975ee7”, 序列化后在java中为一个长度16个字节的字节数组安全

为了节省空间, UUID序列化的规则为: 依次将每2个字符视为一个16进制数, 将其转成对应的10进制数, 并写入一个字节空间中. 总共占16字节ui

一个字节占8个位, 范围为 0000 0000 ~ 1111 1111 (2进制), 00 ~ FF (16进制), 0 ~ 255 (10进制). java里的一个byte变量也能表示256种状态 (恰好至关于16进制数) 然而它的值(10进制)的范围是 -128 ~ 127, 而不是 0 ~ 255. 其中 -128 ~ -1 对应 128 ~ 255this

这就致使了将序列化成byte[]之后的event encode成String的时候出现问题, 由于经常使用的 ASCII, UTF-8等字符集中均没有负数对应的字符. 这意味着event中UUID部分中 80 ~ FF 的值都会被无效encode编码

好比ASCII中这些值会默认被encode成’?’ (字符), decode成java的byte的时候就变成了63(10进制) ; 在UTF-8中更常见的状况是byte[]中的 byte序列不合法 (Invalid byte sequences) 也就是说该序列所表明的值不在UTF-8字符集支持的index范围以内. 致使了原始的byte[]和通过encode decode后的byte[]不一样code

Reference:
java - Encoding and decoding UTF-8 byte arrays from and to strings - Stack Overflow
java - Why are the lengths different when converting a byte array to a String and then back to a byte array? - Stack Overflow

解决方案

  1. 使用Base64安全的转换二进制与字符串, 但会使payload增长33%, 缘由点此

  2. 使用 Latin-1 编码, 最大缺点是解码时对于UTF-8不兼容

  3. 直接传输二进制数据(java中的byte[]), 具体方式为使用jedis中的BinaryClient类, 其中的方法支持 byte[] 类型的参数


For anyone who’s curious enough:

显然方案3是比较理想的. 看到这里记性好的人难免发出疑问: 开头不是说redis只支持String形式的存取吗?

这里引用一段jedis的文档:

A note about String and Binary - what is native?

Redis/Jedis talks a lot about Strings. And here http://redis.io/topics/internals it says Strings are the basic building block of Redis. However, this stress on strings may be misleading. Redis' "String" refer to the C char type (8 bit), which is incompatible with Java Strings (16-bit). Redis sees only 8-bit blocks of data of predefined length, so normally it doesn't interpret the data (it's "binary safe"). Therefore in Java, byte[] data is "native", whereas Strings have to be encoded before being sent, and decoded after being retrieved by the SafeEncoder. This has some minor performance impact. In short: if you have binary data, don't encode it into String, but use the binary versions.

上文提到其实redis官方文档中屡次提到的string是一种误导, 原来redis所说的”String”指的是它的实现语言C中的char (8bit), 对应java中的byte (8bit), 而不是java中的String或char (16bit). Redis只按8位8位地去裸读数据, 而不去解析(所谓的”二进制安全”). 因此, 从java的角度看redis, byte[]类型才是”原生”的

Redis实现中“String”的源码:

struct sdshdr {
    long len;
    long free;
    char buf[];
};

后来想了下, 从传输层面/角度来说, 根本就没有什么类型, 都是1 0. 应时时提醒本身跳出问题以外, 从源头思考, 避免陷入本本主义

相关文章
相关标签/搜索