首頁 > web前端 > js教程 > redis壓縮鍊錶ziplist源碼詳解

redis壓縮鍊錶ziplist源碼詳解

小云云
發布: 2018-01-05 16:43:27
原創
1813 人瀏覽過

壓縮列表(ziplist)是由一系列特殊編碼的記憶體區塊構成的列表,它對於Redis的資料儲存優化有著非常重要的作用本文主要和大家分享redis中使用非常多的一個資料結構壓縮鍊錶ziplist。這個資料結構在redis中說是無所不在也毫不過分,除了鍊錶以外,許多其他資料結構也是用它來過渡的,例如前面文章提到的SortedSet。下面話不多說了,來一起看看詳細的介紹吧。

一、壓縮鍊錶ziplist資料結構簡介

首先從整體來看下ziplist的架構,如下圖:

redis壓縮鍊錶ziplist源碼詳解

# 壓縮鍊錶ziplist結構圖

# 可以看出字段很多,位元組大小也不同,但這也就是壓縮鍊錶的精髓所在了,我們依序總結一下。

 

字段 含義
# zlbytes 此字段是壓縮鍊錶的第一個字段,是無符號整數,佔用4個位元組。用於表示整個壓縮鍊錶所佔用的位元組數(包括它自己)。
# zltail 無符號整數,佔用4個位元組。用於儲存從壓縮鍊錶頭部到最後一個entry(不是尾元素zlend)的偏移量,在快速跳到鍊錶尾部的場景使用。
# zllen 無符號整數,佔用2個位元組。用於儲存壓縮鍊錶中包含的entry總數。
# zlend 特殊的entry,用來表示壓縮鍊錶的末端。佔用一個位元組,值恆為255。
#

總結為ziplist的頭跟尾,以下再總結一下重中之重的entry欄位。

一般來說,一個entry由prevlen,encoding,entry-data三個字段組成,但當entry是個很小的整數時,會根據編碼省略掉entry-data字段。以下依序進行總結:

首先是字段prevlen:表示前一個entry的長度,有兩種編碼方式。

  • 當長度小於255位元組時,用一個位元組儲存。


  • 當長度大於等於255時,用五個位元組進行存儲,其中第一個位元組會被設定為255表示前一個entry的長度由後面四個位元組表示。

然後是字段encoding:它會根據當前元素內容的不同會採用不同的編碼方式,如下:

1.若元素內容為字串,encoding的值分別為:

# 00xx xxxx :00開頭表示字串的長度以6個bit表示。

01xx xxxx | xxxx xxxx :01開頭表示字串的長度由14bit表示,這14個bit採用大端儲存。

1000 0000 | xxxx xxxx | xxxx xxxx | xxxx xxxx | xxxx xxxx :10開頭表示後續的四個位元組為字串長度,這32個bit採用大端儲存。

2.若元素內容為數字,encoding的值分別為:

# 1100 0000:表示數字佔用後面2個位元組。

1101 0000:表示數字佔用後面4個位元組。

1110 0000:表示數字佔用後面8個位元組。

1111 0000 :表示數字佔用後面3個位元組。

1111 1110 :表示數字佔用後面1個位元組。

1111 1111 :表示壓縮鍊錶中最後一個元素(特殊編碼)。

1111 xxxx :表示只用後4位表示0~12的整數,由於0000,1110跟1111三種已經被佔用,也就是說這裡的xxxx四位只能表示0001~1101,轉換成十進制就是數字1~13,但是redis規定它用來表示0~12,因此當遇到這個編碼時,我們需要取出後四位然後減1來得到正確的值。

最後是字段entry-data:如果元素的值為字串,則保存元素本身的值。如果元素的值為很小的數字(按上方編碼規則即0~12),則沒有該欄位。

壓縮鍊錶的編碼非常複雜,但這也正是這個資料結構的精髓所在,一起來看一個例子:

# 註:這個例子是redis原始碼中提到的

//由元素2,5组成的压缩链表
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
 |  |  | | | |
 zlbytes zltail entries "2" "5" end
//字符串"Hello World"编码后的内容
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
登入後複製

上面是一段用十六進位表示2,5兩個元素組成的壓縮鍊錶。

  • 首先前四個位元組表示整個ziplist佔用的位元組數,因為redis採用小端存儲,所以15個位元組表示為0f 00 00 00。


  • 接下來的4個位元組表示末尾元素偏移量,是從ziplist頭(zlbytes)開始到最後一個元素(註:不是尾節點)的偏移量,也是採用小端存儲,因此表示為0c 00 00 00。


  • 再後面是由兩個位元組組成的表示元素個數的zllen字段,在我們這個例子中有兩個元素,加上小端存儲,所以表示為02 00。


  • 接下來是元素本身,首先由一個變長的字端表示前一個元素的長度,2作為第一個元素,它前一個元素的大小就是0,因此佔用一個位元組00。依照我們上面所說的編碼規則,元素2跟5屬於0~12之間的數字,需要使用1111 xxxx格式進行編碼,2轉成二進位為0010,再加上1就是0011(加1的原因上面已經說明),組合起來元素2就表示為00 f3。同理元素5表示為02 f6。


  • 最後就是尾元素,使用未被佔用的編碼1111 1111即255。

接下來我們在這個壓縮鍊錶末尾插入一個字串元素Hello World,先看看該如何編碼該字串,按照約定的編碼規則,首先要用一個字節表示前一個元素的長度,這裡前一個元素時5,總共佔用了兩個字節,因此首先用一個字節節表示前一個元素的長度02,接下來是字串的編碼,我們要加入的字串長度為11(空格也算),轉換成二進位就是1011,依照字串的編碼規則,使用0000 1011表示,轉為十六進位就是0b,最後再加上我們字串本身的ASCII碼,綜合起來就得出該字串的編碼:[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]。

此時整個壓縮鍊錶變成:

[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [02 0b 48 65 6c 6c 6f 20 57 6f 72 6c 64] [ff]
 |  |  | | |   |   |
 zlbytes zltail entries "2" "5"   "Hello World"  end
登入後複製

二、壓縮鍊錶ziplist指令原始碼分析

搞清楚上面的編碼規則以後,我們再來看下壓縮鍊錶ziplist的部分操作源碼,本文就創建壓縮鍊錶,插入元素,刪除元素,查找元素四個操作來總結一下壓縮鍊錶的基本原理。

首先是創建:

//定义由zlbytes,zltail跟zllen组成的压缩链表的头大小
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
//创建一个压缩链表,并且返回指向该链表的指针
unsigned char *ziplistNew(void) {
 //这里之所以+1是因为尾元素占用一个字节,这也是一个压缩链表最小尺寸
 unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
 //分配内存
 unsigned char *zl = zmalloc(bytes);
 //设置链表大小
 ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
 //设置最后一个元素的偏移量
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
 //设置元素个数
 ZIPLIST_LENGTH(zl) = 0;
 //设置尾元素(上面只是申请空间)
 zl[bytes-1] = ZIP_END;
 return zl;
}
登入後複製

建立壓縮鍊錶的邏輯很簡單,就是申請固定的包含頭尾節點的空間,然後初始化鍊錶上下文。

与创建相比,添加元素的源码就非常冗长了,为了便于理解,在看源码之前我们先自己梳理一下添加元素的逻辑。

  • 首先我们要找到指定插入位置的前一个元素的大小,因为该属性是新元素的组成部分之一。


  • 然后我们要对当前元素进行编码来获得相应的encoding字段跟实际元素值的字段。


  • 新插入元素的后继元素的prevlen字段要更新,因为它前面的元素已经改变。这里可能引起级联更新(删除元素也有该问题),原因就是prevlen字段大小是可变的。

上面三步是核心步骤,其余的还有更新尾节点偏移量,修改链表元素个数等操作。当然,由于压缩链表是基于数组实现的,因此在插入或删除元素的时候内存拷贝也是必不可少的。

总结好上面的步骤以后,我们开始一步一步分析源码,比较长,慢慢看:

//四个参数依次是:压缩链表,插入位置(新元素插入p元素后面),元素值,元素长度
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
 //这里是保存当前链表的长度
 size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
 unsigned int prevlensize, prevlen = 0;
 size_t offset;
 int nextdiff = 0;
 unsigned char encoding = 0;
 long long value = 123456789;
 zlentry tail;

 //1. 这段逻辑目的就是获取前置元素的长度
 if (p[0] != ZIP_END) {
 //如果插入位置的元素不是尾元素,则获取该元素的长度
 //这里为了后面使用方便进行了拆分,prevlensize保存encoding字段的长度,prevlen保存元素本身的长度
 ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
 } else {
 //如果插入位置的元素是尾元素,那么需要把新元素插入链表尾端
 //获取到链表最后一个元素(注:最后一个元素不等于尾元素)
 unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);

 if (ptail[0] != ZIP_END) {
  //如果最后一个元素不是尾元素,则该元素为新元素的前置元素,获取该元素长度
  prevlen = zipRawEntryLength(ptail);
 }
 //否则说明链表还没有任何元素,即新元素的前置元素长度为0
 }

 //2. 对新元素进行编码,获取新元素的总大小
 if (zipTryEncoding(s,slen,&value,&encoding)) {
 //如果是数字,则按数字进行编码
 reqlen = zipIntSize(encoding);
 } else {
 //元素长度即为字符串长度
 reqlen = slen;
 }
 //新元素总长度为值的长度加上前置元素跟encoding元素的长度
 reqlen += zipStorePrevEntryLength(NULL,prevlen);
 reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

 //如果插入的位置不是链表尾,则要对新元素的后续元素的prevlen字段进行判断
 //根据上面的编码规则,该字段可能需要扩容
 int forcelarge = 0;
 nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
 if (nextdiff == -4 && reqlen <p>
	分析完插入元素的逻辑,长舒一口气,真的很长,细节也很多。</p><p>
	接下来在再看下删除元素的过程,与添加相比,删除相对要简单一些,清空当前元素以后,需要把后继元素一个一个拷贝上来(这也是数组跟链表两个数据结构的差别),然后注意是否需要级联更新,上代码:</p><pre class="brush:php;toolbar:false">//参数依次为:压缩链表,删除元素的其实位置,删除元素的个数
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
 unsigned int i, totlen, deleted = 0;
 size_t offset;
 int nextdiff = 0;
 zlentry first, tail;
 //读取p指向的元素保存在first中
 zipEntry(p, &first);
 for (i = 0; p[0] != ZIP_END && i  0) {
  if (p[0] != ZIP_END) {
   //判断元素大小是否有改变
   nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
   //修改删除元素之后的元素的prevlen字段
   p -= nextdiff;
   zipStorePrevEntryLength(p,first.prevrawlen);
   //更新末尾元素的偏移量
   ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
   //当删除元素的后继元素不止有一个时,新的末尾元素偏移量需要加上nextdiff
   zipEntry(p, &tail);
   if (p[tail.headersize+tail.len] != ZIP_END) {
    ZIPLIST_TAIL_OFFSET(zl) =
     intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
   }
   //把后面剩余的元素移动至前面
   memmove(first.p,p,
    intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
  } else {
   //直接删除到链表末尾,因此不需要内存拷贝,只需修改最后一个元素的偏移量
   ZIPLIST_TAIL_OFFSET(zl) =
    intrev32ifbe((first.p-zl)-first.prevrawlen);
  }
  //resize数组大小
  offset = first.p-zl;
  zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
  //修改链表元素个数
  ZIPLIST_INCR_LENGTH(zl,-deleted);
  p = zl+offset;
  //nextdiff != 0表示元素大小发生变化,需要进行级联更新
  if (nextdiff != 0)
   zl = __ziplistCascadeUpdate(zl,p);
 }
 return zl;
}
登入後複製

最后我们看下元素的查找操作:

//参数依次为:压缩链表,要查找元素的值,要查找元素的长度,每次比较之间跳过的元素个数
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
 int skipcnt = 0;
 unsigned char vencoding = 0;
 long long vll = 0;
 //只要还没到尾元素就不断循环
 while (p[0] != ZIP_END) {
  unsigned int prevlensize, encoding, lensize, len;
  unsigned char *q;
  //查询链表当前元素的prevlen字段
  ZIP_DECODE_PREVLENSIZE(p, prevlensize);
  //查询链表当前元素的encoding字段
  ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
  q = p + prevlensize + lensize;
  //已到达需要比较的元素位置
  if (skipcnt == 0) {
   //如果链表中的当前元素时字符串
   if (ZIP_IS_STR(encoding)) {
    //跟要查找的字符串进行比较
    if (len == vlen && memcmp(q, vstr, vlen) == 0) {
     //匹配成功,则要查找元素的指针
     return p;
    }
   } else {
    //如果当前元素为数字且vencoding为0
    if (vencoding == 0) {
     //尝试对要查找的值进行数字编码
     if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
      //如果编码失败,则说明要查找的元素根本不是数字
      //然后把vencoding设置为最大值,起一个标记作用
      //也就是说后面就不用尝试把要查找的值编码成数字了
      vencoding = UCHAR_MAX;
     }
     assert(vencoding);
    }
    //如果vencoding != UCHAR_MAX,则说明要查找的元素成功编码为数字
    if (vencoding != UCHAR_MAX) {
     //按数字取出当前链表中的元素
     long long ll = zipLoadInteger(q, encoding);
     if (ll == vll) {
      //如果两个数字相等,则返回元素指针
      return p;
     }
    }
   }
   //重置需要跳过的元素个数
   skipcnt = skip;
  } else {
   //跳过元素
   skipcnt--;
  }
  //遍历下个元素
  p = q + len;
 }
 //遍历完整个链表,没有找到元素
 return NULL;
}
登入後複製

到这里就把压缩链表的创建,添加,删除,查找四个基本操作原理总结完了。

三、压缩链表ziplist数据结构总结

压缩链表ziplist在redis中的应用非常广泛,它算是redis中最具特色的数据结构了。该数据结构的精髓其实就在于文章第一部分总结的编码规则,先理清楚这部分内容,后面的源码可以简单看下加深理解。

不得不说源码部分着实有点冗长,确实需要耐心,我自己在读的过程中也很头大。如果对源码感兴趣的话,建议按我的方法,先自己梳理某个操作(例如上面提到的插入元素)需要做哪些事情,然后再看代码可能会更好理解一些。

相关推荐:

深入剖析 redis 数据结构 ziplist

Redis源码分析(六)---ziplist压缩列表

Redis内部数据结构详解之ziplist

以上是redis壓縮鍊錶ziplist源碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板