字符編碼問題記錄

需求&問題

須要對序列化之後的對象 (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. 應時時提醒本身跳出問題以外, 從源頭思考, 避免陷入本本主義

相關文章
相關標籤/搜索