[深入理解Redis]讀取RDB檔案

hoohack發表於2019-03-04

最近在做一個解析rdb檔案的功能,途中遇到了一些問題,也解決了一些問題。具體為什麼要做這件事情之後再詳談,本次主要想聊聊遇到的開始處理檔案時遇到的第一個難題:理解RDB檔案的協議、如何讀取二進位制檔案。

RDB檔案

[Redis原始碼閱讀]redis持久化
文章介紹過,Redis的持久化是通過RDB和AOF實現的。Redis的RDB檔案是二進位制格式的檔案,從這個方面再次體現了Redis是基於記憶體的快取資料庫,不管對於儲存到硬碟還是恢復資料都十分快捷。Redis有多種資料型別:string、list、hash、set、zset,不同資料型別佔用的記憶體大小是不一樣的,解析出自然語言可以識別的資料就需要使用不同的方法,儲存到檔案的時候也需要一些協議或者規則。這有點類似於程式語言裡面的資料型別,不同的資料型別佔用的位元組大小不一致,但是儲存到計算機都是二進位制的編碼,就看是讀取多少個位元組,以怎樣的方式解讀。

舉個例子,redis的物件型別是特定的幾個字元表示,0代表字串,讀取到字串型別後,緊接著就是字串的長度,儲存著接下來需要讀取的位元組大小,讀取到的位元組最終構成完整字串物件的值。對於儲存了"name" => "hoohack"鍵值對的字串物件儲存到記憶體可以用下圖表示:

redis字串儲存

當然,除了字串,redis還有列表,集合,雜湊等各種物件,針對這些型別,在RDB檔案裡面都有不同的規則定義,只需要按照RDB檔案格式的協議來解讀檔案,就能完整無誤地把檔案解讀成自然語言能描述的字元。

仔細對比,可以發現跟計算機的操作方式是類似的,資料儲存在計算機都是二進位制的,具體的值應該看需要多少個位元組,以什麼型別解析,讀取不同的位元組解析到的值是不一樣的,同樣的位元組大小,但是使用不同型別儲存,只要做適當的轉換,也是正確的。比如在C語言中的void *指標是4個位元組,int也是4個位元組,定義一個int整數,將它儲存到void *也是沒問題的,讀取的時候只需要做一次型別轉換就可以了。

因此,解讀RDB檔案最關鍵的就是理解RDB檔案的協議,只要理解完RDB檔案格式的協議,根據定義好的協議來解析各種資料型別的資料。更詳細的RDB檔案協議可以參考RDB檔案格式的定義文件:RDB file format

檢視RDB檔案

先清空redis資料庫,儲存一個鍵值對,然後執行save命令將當前資料庫的資料儲存的rdb檔案,得到檔案dump.rdb。

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set name hoohack
OK
127.0.0.1:6379> save
OK
複製程式碼

cat檢視檔案:

cat-rdb-file

可以看到是一個包含亂碼的檔案,因為檔案是以二進位制的格式儲存,要想列印出人類能看出的語言,可以通過linux的od命令檢視。od命令用於輸出檔案的八進位制、十六進位制或其他格式編碼的位元組,通常用於輸出檔案中不能直接顯示在終端的字元,注意輸出的是位元組,分隔符之間的字元都是儲存在一個位元組的。

od命令輸出檔案八進位制、十六進位制等格式

通過man手冊可以看到,列印出16進位制格式的引數是x,字元是c,將字元與十六進位制的對應關係列印出來:od -A x -t x1c -v dump.rdb

列印得出結果如下:

linux-od

從上圖看到,檔案列印出來的都是一些十六進位制的數字,轉換成十進位制再去ASCII碼錶就能查詢到對應的字元。比如第一個字元,52=5*16+2*1=82=`R`
在這裡說一句,個人覺得這od命令非常有用,在解析資料出現疑惑的時候,就是通過這個命令排查遇到的問題。把檔案內容列印出來,找到當前讀取的字元,對比RDB檔案協議,看看究竟要解析的是什麼資料,再對比程式碼中的解析邏輯,看看是否有問題,然後再修正程式碼。
如果覺得敲命令麻煩,可以把檔案上傳然後線上檢視:www.onlinehexeditor.com

資料的二進位制儲存和讀取

我們知道,計算機的所有資料都是以二進位制的格式儲存的,我們看到的字元是通過讀取二進位制然後解析出來的資料,程式根據不同資料型別做相應的轉換,然後展示出來的就是我們看到的字元。
計算機允許多種資料型別,比如有:32位整數、64位整數、字串、浮點數、布林值等等,不同的資料型別是通過讀取不同大小的位元組,根據型別指定的讀取方式讀取出來,就是想要的資料了。

解析資料的第一步,就是讀取資料。在計算機裡面,大家所知道的資料都是逐個位元組地讀取資料。因此,首先要實現的就是按照位元組去讀取檔案。

本次採用實現的解析RDB檔案功能的語言是Golang,在Golang的檔案操作的API裡,提供了按位元組讀取的函式File.ReadAt

函式原型如下:

func (f *File) ReadAt(b []byte, off int64) (n int, err error)
複製程式碼

函式從File指標的指向的位置off開始,讀取len(b)個位元組的資料,並儲存到b中。
根據對API的理解,自己實現了一個按照位元組讀取檔案資料的函式,函式接收長度值,代表需要讀取的位元組長度。具體實現程式碼如下:

type Rdb struct {
    fp *os.File
    ... // other field
}

func (r *Rdb) ReadBuf(length int64) ([]byte, error) {
    // 初始化一個大小為length的位元組陣列
    buf := make([]byte, length)

    // 從curIndex開始讀取length個位元組
    size, err := r.fp.ReadAt(buf[:length], r.curIndex)

    checkErr(err)

    if size < 0 {
        fmt.Fprintf(os.Stderr, "cat: error reading: %s
", err.Error())
        return []byte{}, err
    } else {
        // 讀取成功,更新檔案操作的偏移量
        r.curIndex += length
        return buf, nil
    }
}
複製程式碼

Golang的資料轉換

上面實現的函式返回的是位元組陣列,當函式返回讀取到的資料後,如果需要儲存在不同的資料型別就需要做轉換,Golang也提供了比較強大的api。以下是我在解析資料時遇到的資料型別轉換的解決方案,希望對大家有幫助。
字串

str := string(buf)
複製程式碼

int整數,先轉為二進位制的值,然後再用int32型別格式化

intVal := int(binary.BigEndian.Uint32(buf))
複製程式碼

int64整數,先轉為二進位制的值,然後再用int64型別格式化

int64Val := int64(int16(binary.LittleEndian.Uint16(valBuf)))
複製程式碼

浮點數

floatVal, err := strconv.ParseFloat(string(floatBuf), 64)
複製程式碼

float64浮點數,先轉為二進位制的值,再呼叫math庫的Float64frombits函式轉換二進位制的值為float64型別

floatBit := binary.LittleEndian.Uint64(buf)
floatVal := math.Float64frombits(floatBit)
複製程式碼

總結

理論上的理解和實踐上的應用是不一樣的,雖然大家都知道資料是二進位制的,就是怎麼怎麼解析,但是真正實現起來還是不少問題。通過操作二進位制檔案的一次實踐,收穫了以下幾點:

1、更深刻地理解到資料在計算機中的儲存方式,一切都是0和1的二進位制內容,只是看你要怎麼用而已,適當的類似轉換也可以得到你想要的內容

2、在某個系統下操作就要遵循已定義好的協議,不然得到的都是亂套或者亂碼的東西,比如資料的位元組序不同也會使資料的解析結果不一致

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,請點個贊吧,謝謝^_^

更多精彩內容,請關注個人公眾號。

[深入理解Redis]讀取RDB檔案

相關文章