[深刻理解Redis]读取RDB文件

最近在作一个解析rdb文件的功能,途中遇到了一些问题,也解决了一些问题。具体为何要作这件事情以后再详谈,本次主要想聊聊遇到的开始处理文件时遇到的第一个难题:理解RDB文件的协议、如何读取二进制文件。html

RDB文件

[Redis源码阅读]redis持久化
文章介绍过,Redis的持久化是经过RDB和AOF实现的。Redis的RDB文件是二进制格式的文件,从这个方面再次体现了Redis是基于内存的缓存数据库,无论对于存储到硬盘仍是恢复数据都十分快捷。Redis有多种数据类型:string、list、hash、set、zset,不一样数据类型占用的内存大小是不同的,解析出天然语言能够识别的数据就须要使用不一样的方法,保存到文件的时候也须要一些协议或者规则。这有点相似于编程语言里面的数据类型,不一样的数据类型占用的字节大小不一致,可是保存到计算机都是二进制的编码,就看是读取多少个字节,以怎样的方式解读。linux

举个例子,redis的对象类型是特定的几个字符表示,0表明字符串,读取到字符串类型后,紧接着就是字符串的长度,保存着接下来须要读取的字节大小,读取到的字节最终构成完整字符串对象的值。对于保存了"name" => "hoohack"键值对的字符串对象保存到内存能够用下图表示:redis

redis字符串存储

固然,除了字符串,redis还有列表,集合,哈希等各类对象,针对这些类型,在RDB文件里面都有不一样的规则定义,只须要按照RDB文件格式的协议来解读文件,就能完整无误地把文件解读成天然语言能描述的字符。数据库

仔细对比,能够发现跟计算机的操做方式是相似的,数据保存在计算机都是二进制的,具体的值应该看须要多少个字节,以什么类型解析,读取不一样的字节解析到的值是不同的,一样的字节大小,可是使用不一样类型保存,只要作适当的转换,也是正确的。好比在C语言中的void *指针是4个字节,int也是4个字节,定义一个int整数,将它保存到void *也是没问题的,读取的时候只须要作一次类型转换就能够了。编程

所以,解读RDB文件最关键的就是理解RDB文件的协议,只要理解完RDB文件格式的协议,根据定义好的协议来解析各类数据类型的数据。更详细的RDB文件协议能够参考RDB文件格式的定义文档:RDB file formatapi

查看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=516+21=82='R’。
在这里说一句,我的以为这od命令很是有用,在解析数据出现疑惑的时候,就是经过这个命令排查遇到的问题。把文件内容打印出来,找到当前读取的字符,对比RDB文件协议,看看究竟要解析的是什么数据,再对比代码中的解析逻辑,看看是否有问题,而后再修正代码。
若是以为敲命令麻烦,能够把文件上传而后在线查看:https://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\n", 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)

总结

理论上的理解和实践上的应用是不同的,虽然你们都知道数据是二进制的,就是怎么怎么解析,可是真正实现起来仍是很多问题。经过操做二进制文件的一次实践,收获了如下几点:

一、更深入地理解到数据在计算机中的保存方式,一切都是0和1的二进制内容,只是看你要怎么用而已,适当的相似转换也能够获得你想要的内容

二、在某个系统下操做就要遵循已定义好的协议,否则获得的都是乱套或者乱码的东西,好比数据的字节序不一样也会使数据的解析结果不一致

原创文章,文笔有限,才疏学浅,文中如有不正之处,万望告知。

若是本文对你有帮助,请点个赞吧,谢谢^_^

更多精彩内容,请关注我的公众号。

相关文章
相关标签/搜索