零 版本
Lucene-Core 版本 8.8.2
一 簡介
Lucene 的 Index 設計基本依賴磁碟儲存,而倒排索引是依賴大量冗餘資料來完成分詞搜尋的技術,所以 Lucene 在設計的時候用了很多時間換空間的資料壓縮技術,以此保障能在最少的磁碟資源來儲存最多的資料。VInt 就是其中一個很有意思的結構設計。
二 技術原理
1 概要
Java 中一個普通的 int 佔據 4 個 byte。
但是當 int 的值為 -128 ~ 127 的時候,其實只需要一個 byte 就可以放得下了,其它三個 byte 都是無意義的冗餘(其它幾個 byte 所能代表的區間以此類推),真實能夠用滿這四個 byte 的情況並不多。VInt 的意思是 variant int,也就是可變的 int。其本質是按需分配,減少這種冗餘。
2 byte 指示位
一個正常的 byte 有八個資料有效位,而 VInt 中只有七個,最高位變成了後一個 byte 的指示位。
- 最高位為 1,代表後一個 byte 依然是當前資料
最高位為 0,代表後面沒有資料了
3 VInt 的副作用
- 對於正數來說,由於只有七個資料位,所以當 int 的值比較大的時候,可能會需要 5 個 byte 才能表述當前的資料(這個問題無法被解決,VInt 也覺得無需解決,因為情況在真實生產中並不多)
對於負數來說,最高位為 1,無法被壓縮(引入 zigzag 編碼)
4 zigzag 編碼
使用位移和異或操作將首位的符號位挪到資料的最後一位。
三 Demo
假如需要分別序列化 1 / 200 / -1 這三個 int 數,則 VInt 演算法的具體步驟為(有效資料標黃):
1 二進位制化
- 1 的二進位制數為 00000000 00000000 00000000 00000001
- 200 的二進位制數為 00000000 00000000 00000000 11001000
-1 的二進位制數為 11111111 11111111 11111111 11111110
2 向前位移一位,後面補 0
- 1 處理後的二進位制數為 00000000 00000000 00000000 00000010
- 200 處理後的二進位制數為 00000000 00000000 00000001 10010000
-1 處理後的二進位制數為 11111111 11111111 11111111 11111100
3 異或操作
異或操作的本質是不同為 0,相同是 1。
對於正數,異或一個 11111111 11111111 11111111 11111111
- 1 的處理表示式為 00000000 00000000 00000000 00000010 ^ 11111111 11111111 11111111 11111111 = 00000000 00000000 00000000 00000010;
- 200 的處理表示式為 00000000 00000000 00000001 10010000 ^ 11111111 11111111 11111111 11111111 = 00000000 00000000 00000001 10010000
對於負數,異或一個 00000000 00000000 00000000 00000000
-1 的處理表示式為 11111111 11111111 11111111 11111100 ^ 00000000 00000000 00000000 00000000 = 00000000 00000000 00000000 00000011
4 八位為一個單位處理數字
以八位為一個單位讀取資料,當讀取到八位之後,將第一位看作是標記位,如果還有其它資料的話,再讀取八位。
對於數字 1 來說
序列化過程:
- 先讀取七位 0000010,之前都為 0,沒有資料,則前面補 0,為 00000010
讀取過程:
- 讀取序列化資料 00000010,第一位是 0,代表只有一個 byte,後面沒有其它資料,故資料為 00000010
- 最後一位是 0,代表是正數,與 11111111 異或操作,得到 00000010
- 將資料往後挪一位,前端補 0,最終為 00000001
對於數字 200 來說
序列化過程:
- 先讀取七位 0010000,之前不都為 0,則前面補 1,為 10010000
- 再讀取七位 0000011,之前都為 0,則前面補 0,為 00000011
- 組合資料為 10010000 00000011
讀取過程:
- 讀取序列化資料 10010000,第一位是 1,代表不止一個 byte,後面還有其它資料,故資料為 0010000
- 再讀取 00000011,第一位是 0,代表沒有其它資料來,資料為 0000011
- 組合資料為 00000001 10010000
- 最後一位是 0,代表是正數,與 11111111 11111111 異或操作,得到 00000001 10010000
- 將資料往後挪一位,前端補 0,最終為 00000000 11001000
對於數字 -1 來說
序列化過程:
- 先讀取七位 0000011,之前都為 0,沒有資料,則前面補 0,為 00000011
讀取過程:
- 讀取序列化資料 00000011,第一位是 0,代表只有一個 byte,後面沒有其它資料,故資料為 00000011
- 最後一位是 1,代表是負數,與 00000000 異或操作,得到 11111100
將資料往後挪一位,前端補 1,最終為 11111110
四 原始碼
0 流程
以下原始碼的呼叫流程:
- lucene 確認一個 int 值
- 呼叫 zigZagEncode(...) 將 int 編碼成 zint
- 呼叫 writeVInt(...) 方法將 zint 編碼成 vint,並寫入到磁碟或者其它記憶體容器中
- 呼叫 readVInt() 方法從磁碟或者其它記憶體容器中讀取一個 vint 值,並將其反編碼成 zint
呼叫 zigZagDecode(...) 方法將 zint 反編碼成 int
1 writeZInt
writeZInt(...) 方法在 org.apache.lucene.store.DataOutput 中:
// 這個方法用於寫入一個 zigzag 編碼之後的 int 值 public final void writeZInt(int i) throws IOException { // BitUtil.zigZagEncode(i) 用於 zigzag 編碼 writeVInt(BitUtil.zigZagEncode(i)); } // 用於寫入一個 VInt public final void writeVInt(int i) throws IOException { while ((i & ~0x7F) != 0) { // writeByte(...) 方法用於將 byte 持久化到檔案中,暫時無需關注 writeByte((byte)((i & 0x7F) | 0x80)); i >>>= 7; } writeByte((byte)i); }
2 zigZagEncode
zigZagEncode(...) 方法在 org.apache.lucene.util.BitUtil 中:
// i >> 31 對於正數或者 0 來說,會返回全 0 的屏障 // i >> 31 對於負數來說,會返回全 1 的屏障 public static int zigZagEncode(int i) { return (i >> 31) ^ (i << 1); }
3 readZInt
readZInt(...) 方法在 org.apache.lucene.store.DataOutput 中:
public int readZInt() throws IOException { return BitUtil.zigZagDecode(readVInt()); } public int readVInt() throws IOException { // 此處從磁碟讀取一個 byte byte b = readByte(); // b >= 0,代表最高位是 0,後續沒有值了,以下雷同 if (b >= 0) return b; int i = b & 0x7F; // 繼續讀取一個 byte b = readByte(); i |= (b & 0x7F) << 7; if (b >= 0) return i; // 繼續讀取一個 byte b = readByte(); i |= (b & 0x7F) << 14; if (b >= 0) return i; // 繼續讀取一個 byte b = readByte(); i |= (b & 0x7F) << 21; if (b >= 0) return i; // 繼續讀取一個 byte,在 VInt 的編碼下,最高五個 byte b = readByte(); i |= (b & 0x0F) << 28; if ((b & 0xF0) == 0) return i; throw new IOException("Invalid vInt detected (too many bits)"); }
4 zigZagDecode
zigZagDecode(...) 方法在 org.apache.lucene.util.BitUtil 中:
// decode 的操作和 zigZagEncode(...) 是完全相反的 public static int zigZagDecode(int i) { return ((i >>> 1) ^ -(i & 1)); }