圧縮リスト (ziplist) は、特別にエンコードされた一連のメモリ ブロックで構成されるリストで、Redis データ ストレージの最適化において非常に重要な役割を果たします。この記事では主に、Redis でよく使用されるデータ構造である ziplist について説明します。 。このデータ構造は、リンク リストに加えて、前の記事で説明した SortedSet など、他の多くのデータ構造でも redis のいたるところにあると言っても過言ではありません。以下では多くを語る必要はありません。詳細な紹介を見てみましょう。
1. 圧縮リンクリスト ziplist データ構造の紹介
まず、以下に示すように、ziplist 全体の構造を見てみましょう:
圧縮リンクリストのジップリスト構造図
多くのフィールドと異なるバイト サイズがあることがわかりますが、これが圧縮リンク リストの本質です。これらを 1 つずつまとめてみましょう。
フィールド | 意味 |
---|---|
ズルバイト | このフィールドは、圧縮リンク リストの最初のフィールドであり、符号なし整数であり、4 バイトを占めます。圧縮されたリンク リスト全体 (それ自体を含む) が占めるバイト数を表すために使用されます。 |
符号なし整数。4 バイトを占有します。圧縮されたリンク リストの先頭から最後のエントリ (末尾要素 zlend ではない) までのオフセットを格納するために使用され、リンク リストの末尾にすぐにジャンプできるシナリオで使用されます。 | |
これはジップリストの先頭と末尾としてまとめられており、最も重要な入力フィールドは以下にまとめられています。
一般に、エントリは prevlen、encoding、entry-data の 3 つのフィールドで構成されます。ただし、エントリが小さい整数の場合、entry-data フィールドはエンコーディングに基づいて省略されます。以下に要約します:
1 つ目はフィールド prevlen です。これは、前のエントリの長さを示します。エンコード方法は 2 つあります。
255バイト未満の場合は1バイトで格納されます。
長さが 255 以上の場合、5 バイトがストレージに使用され、最初のバイトは 255 に設定され、前のエントリの長さが次の 4 バイトで表されることを示します。
次に、フィールドのエンコーディングがあります。次のように、現在の要素の内容に応じて異なるエンコーディング方法が使用されます。 1. 要素の内容が文字列の場合、エンコード値は次のとおりです:
00xx xxxx: 00 で始まる文字列の長さが 6 ビットで表されることを示します。
01xx xxxx |
| 1000 0000 | xxxx xxxx |
2. 要素の内容が数値の場合、エンコード値は次のとおりです:
1100 0000: 数値が次の 2 バイトを占めることを示します。
1101 0000: 数値が次の 4 バイトを占めることを示します。
1110 0000: 数値が次の 8 バイトを占めることを示します。
1111 0000: 数値が次の 3 バイトを占めることを示します。
1111 1110: 数値が次のバイトを占めることを示します。
1111 1111: 圧縮リンク リストの最後の要素を示します (特殊なエンコーディング)。
1111xxxx :0~12の整数を下4桁のみで表現することを示します。0000、1110、1111は既に占有されているため、ここでのxxxxの4桁は0001~1101しか表現できません。10進数に変換するとただし、redis では 0 ~ 12 を表すために使用されると規定されているため、このエンコードに遭遇した場合は、最後の 4 桁を取り出して 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 つの要素 2 と 5 で構成される圧縮リンク リストを 16 進数で表したものです。
この時点で、圧縮されたリンク リスト全体は次のようになります:
[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
2. 圧縮リンクリスト ziplist コマンドのソースコード解析
上記のエンコード規則を理解した後、圧縮リンク リスト ziplist の操作ソース コードの一部を見てみましょう。この記事では、圧縮リンク リストの作成、要素の挿入、削除の 4 つの操作を通じて、圧縮リンク リストの基本原理をまとめます。要素、および要素の検索。
まず、
//定义由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ソースコードの詳細説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。