深入了解Redis中Hash底层结构

83 阅读5分钟
  • 存储类型与操作命令
类型(Type key)存储编码(OBJECT encoding key)实例命令
hashziplist(压缩列表) 压缩列表本质上就是一个字节数组,是Redis为了节约内存 而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一 个字节数组或一个整数,Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。 当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构hmset person name zhangsan gender 1 age 22
hashhashtableset h1 name xxx(超过64字节)

问题:hash什么时候存ziplist?(反之破坏了条件即存hashtable)

1.一个hash对象保存的field数量<512个

2.一个hash对象中所有的field和value的字符串长度都<64字节

存储原理

  • ziplist
typedef struct zlentry {
    unsigned int prevrawlensize; /*存储上一个链表节点的长度数值所需的字节*/
    unsigned int prevrawlen;  /*上一个链表节点占用的长度*/   
    unsigned int lensize;  /*存储当前链表节点长度数值所需要的字节数*/      
    unsigned int len;    /*当前链表节点占用的长度*/        
    unsigned int headersize;    /*当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小*/ 
    unsigned char encoding;  /*编码方式*/    
    unsigned char *p;    /*压缩链表以字符串的形式保存,该指针指向当前节点起始位置*/       
} zlentry;

  • 结构示意图

ziplist编码结构图.png

  • 压缩状态属性说明
属性说明
previous_entry_lengthprevious_entry_length字段表示前一个元素的字节长度,占1个或者5个字节
如果当前一个元素的长度 小于254字节时,用1个字节表示,如:previous_entry_length:0x05表示前一个节点长度为5字节
如果当前一个元素的长度大于或等于254字节时,用5个字节来表示。而 此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长 ,如:previous_entry_length:0xFE00002766表示这个是一个5字节长的previous_entry_length属性,前一个节点的实际长度为:0x00002766(十进制10086)
因为节点的 previous_entry_length记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,举例如果有一个指向当前节点起始地址的指针c,那么我们只要用指针c减去当前节点previous_entry_length属性值,就可以得出一个指向前一个节点起始地址的指针p ;p = c - previous_entry_length
encodingencoding属性记录了节点的conten属性所保存数据的类型及长度:
一字节、两字节、或者五字节长,值得最高位00、01或者10的是字节数组编码,这种编码表示节点的content属性保存着字节数组,数组的长度由编码去除最高两位之后的其他位记录
一字节长,值得最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数数值,整数值得类型和长度由编码除去最高位之后的其他位记录
content节点的content属性负责保存节点的值,节点的值可以是一个字节数组或者整数,值得类型和长度由节点的encoding属性决定
  • encoding

    字节数组编码

编码编码长度content属性保存的值
00bbbbbb1字节长度小于等于63字节的字节数组
01bbbbbb xxxxxxxx2字节长度小于等于16383字节的字节数组
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx5字节长度小于等于4294967295的字节数组

整数编码

编码编码长度
110000001字节int16_t类型的整数
110100001字节int32_t类型的整数
111000001字节int64_t类型的整数
111100001字节24位有符号整数
111111101字节8位有符号整数
1111xxxx1字节使用这一编码的节点没有相应的content属性,因为编码本身的xxxx四个为已经保存一个介于0和12之间的值,所以它无须conten属性

ziplist压缩示例图.png

  • 解压状态属性说明
属性说明
zlbytes压缩列表的字节长度,占4个字节,因此压缩列表最多有232^{32} -1个字节
zltail压缩列表尾元素相对于压缩列表起始地址的偏移量,通过这个偏移量程序无需遍历整个压缩列表就可以确定表尾节点的地址,占4个字节
zllen压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(2162^{16} -1)的压缩 列表,必须遍历整个压缩列表才能获取到元素个数
entryX压缩列表存储的元素,可以是字节数组或者整数,长度不限
zlend压缩列表的结尾,占1个字节,恒为0xFF
  • 压缩与解压编码之间的对应关系(源码)

previous_entry_length字段的长度(prevrawlensize)、previous_entry_length字段存储的内容(prevrawlen)、encoding字段的 长度(lensize)、encoding字段的内容(len表示元素数据内容的长 度,encoding表示数据类型)和当前元素首地址(p);而headersize则 表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和

void zipEntry(unsigned char *p, zlentry *e) {
       ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
       ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
       e->headersize = e->prevrawlensize + e->lensize;
       e->p = p;
}
//解码previous_entry_length字段,此时入参ptr指向元素首地址
define ZIP_BIG_PREVLEN 254
define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {
       if ((ptr)[0] < ZIP_BIG_PREVLEN) {
           (prevlensize) = 1;
           (prevlen) = (ptr)[0];
       } else {
           (prevlensize) = 5;
           memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);
           memrev32ifbe(&prevlen);
  }
} while(0);
//解码encoding字段逻辑,此时入参ptr指向元素首地址偏移 previous_entry_length字段长度的位置
#define ZIP_STR_MASK 0xc0
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { (encoding) = (ptr[0]);
// ptr[0]<11000000说明是字节数组,前两个比特为字节数组编码类型
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; if ((encoding) < ZIP_STR_MASK) {
           if ((encoding) == ZIP_STR_06B) {
               (lensize) = 1;
               (len) = (ptr)[0] & 0x3f;
           } else if ((encoding) == ZIP_STR_14B) {
               (lensize) = 2;
               (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];
           } else if ((encoding) == ZIP_STR_32B) {
               (lensize) = 5;
               (len) = ((ptr)[1] << 24) |
            } else {
							((ptr)[2] << 16) |
							((ptr)[3] <<  8) |
							((ptr)[4]);
              panic("Invalid string encoding 0x%02X", (encoding));
           }
       } else {
           (lensize) = 1;
           (len) = zipIntSize(encoding);
       }
} while(0);
//字节数组只根据ptr[0]的前2个比特即可判断类型,而判断整数类 型需要ptr[0]的前4个比特,代码如下
unsigned int zipIntSize(unsigned char encoding) {
    switch(encoding) {
    case ZIP_INT_8B:  return 1;
    case ZIP_INT_16B: return 2;
    case ZIP_INT_24B: return 3;
    case ZIP_INT_32B: return 4;
    case ZIP_INT_64B: return 8;
    }
//0~12立即数
if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
        return 0;
    panic("Invalid integer encoding 0x%02X", encoding);
    return 0;
}
  • hashtable
typedef struct dict {
    dictType *type; //字典类型
    void *privdata; //私有数据
    dictht ht[2]; //一个字典有两个哈希表
    long rehashidx; //rehash索引 /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; //当前正在使用的迭代器数量/* number of iterators currently running */
} dict;

typedef struct dictht {
    dictEntry **table; // 哈希表数组
    unsigned long size; // 哈希表大小
    unsigned long sizemask; //掩码大小,用于计算索引值,总等于size -1
    unsigned long used; //已有节点数
} dictht;

typedef struct dictEntry {
    void *key; //key关键字定义
    union {
        void *val; //value定义
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; //指向下一个键值对节点
} dictEntry;

  • 结构示意图

hash源码架构图.png

应用场景

  • 以对象的方式存储,利用hash的命令进行操作