English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Anleitung zur Analyse des Redis-Quellcodes - Detailanalyse der compressierten Liste ziplist

Einführung

Die komprimierte Liste (ziplist) besteht aus einer Reihe speziell codierter Speicherblöcke und spielt eine sehr wichtige Rolle bei der Datenstoredoptimierung von Redis. In diesem Artikel werde ich eine der häufig verwendeten Datenstrukturen in Redis, die komprimierte Liste ziplist, zusammenfassen. Diese Datenstruktur ist in Redis überall präsent, was nicht übertreiben ist, außer der Liste, viele andere Datenstrukturen verwenden sie auch als Übergang, wie die im vorliegenden Artikel erwähnte SortedSet. Viel reden ist nicht mehr notwendig, lasst uns gemeinsam einen detaillierten Überblick darüber geben.

1. Kurze Einführung in die Datenstruktur der komprimierten Liste ziplist

Zunächst betrachten wir die Struktur der ziplist im Ganzen, wie im folgenden Bild gezeigt:

Strukturdiagramm der komprimierten Liste ziplist

Es ist erkennbar, dass viele Felder vorhanden sind und die Größen der Bytes unterschiedlich sind, aber das ist auch der Kern der komprimierten Liste. Wir fassen dies nacheinander zusammen.

 

Feld Bedeutung
zlbytes Dieses Feld ist der erste Feld der komprimierten Liste und ist ein unsignierter Integer, der4Bytes. Verwendet zur Angabe der Anzahl der Bytes, die die gesamte komprimierte Liste belegt (einschließlich ihrer selbst).
zltail Unsignierte Integer, die4Bytes belegen. Verwendet zur Speicherung des Offset von der Kopfenden der komprimierten Liste bis zum letzten entry (nicht dem Elements zlend) für den schnellen Sprung zum Ende der Liste.
zllen Unsignierte Integer, die2Bytes. Verwendet zur Speicherung der Anzahl der enthaltenen entrys in der komprimierten Liste.
zlend Besondere entrys, die zur Darstellung des Endes der komprimierten Liste verwendet werden. Sie belegen einen Byte und den Wert255。

Zusammengefasst: Kopf und Ende des ziplist, ferner eine Zusammenfassung des wichtigsten Felds entry.

In der Regel besteht ein entry aus prevlen, encoding und entry-data三个字段组成,但当entry是个很小的整数时,会根据编码省略掉entry-data字段。下面依次进行总结:

drei Felder data bestehen, aber wenn der entry eine sehr kleine Ganzzahl ist, wird er nach der Kodierung weggelassen.

  • data-Feld. Hier folgt eine Zusammenfassung:255Der Anfang ist das Feld prevlen:stellt die Länge des vorherigen entry dar, es gibt zwei Kodierungsarten.
  • Bytes gespeichert wird.255Bytes gespeichert wird.255gespeichert wird, wenn die Länge größer oder gleich

dann wird mit fünf Bytes gespeichert, wobei der erste Byte auf

1、wenn der Inhalt des Elements eine Zeichenkette ist, die Werte von encoding sind:

00xx xxxx :00 beginnt mit der Angabe, dass die Länge der Zeichenkette mit6Bits dargestellt wird.

01xx xxxx | xxxx xxxx :01Der Anfang zeigt an, dass die Länge der Zeichenkette durch14Bits darstellen, das14Bits verwenden großen Endian Speicherung.

1000 0000 | xxxx xxxx | xxxx xxxx | xxxx xxxx | xxxx xxxx :10Der Anfang zeigt an, dass die nächsten vier Bytes die Länge der Zeichenkette sind, das32Bits verwenden großen Endian Speicherung.

2、wenn der Inhalt des Elements eine Zahl ist, die Werte von encoding sind:

1100 0000:stellt die Anzahl der Bytes, die von der Nummer belegt werden, dahinter2Bytes.

1101 0000:stellt die Anzahl der Bytes, die von der Nummer belegt werden, dahinter4Bytes.

1110 0000:stellt die Anzahl der Bytes, die von der Nummer belegt werden, dahinter8Bytes.

1111 0000 :stellt die Anzahl der Bytes, die von der Nummer belegt werden, dahinter3Bytes.

1111 1110 :stellt die Anzahl der Bytes, die von der Nummer belegt werden, dahinter1Bytes.

1111 1111 :stellt den letzten Element der komprimierten Liste (spezielle Kodierung) dar.

1111 xxxx :stellt nur die letzten4stellen darstellen 0~12von Ganzzahlen, da 0000,1110und1111Drei sind bereits belegt, das bedeutet, dass die vier Stellen xxxx hier nur 000 darstellen können1~1101zu darstellen, in Dezimalzahl ist dies die Zahl1~13,aber Redis bestimmt, dass es verwendet wird, um 0~12,daher müssen wir, wenn wir auf diese Kodierung stoßen, die letzten vier Stellen abziehen und1um den richtigen Wert zu erhalten.

Schließlich ist das Feld entry-data:Wenn der Wert des Elements eine Zeichenkette ist, wird der Wert des Elements selbst gespeichert. Wenn der Wert des Elements sehr kleine Zahlen sind (gemäß den obigen Kodierungsregeln sind dies 0~12),dann gibt es kein solches Feld.

Die Kodierung der komprimierten Liste ist sehr komplex, aber das ist auch der Kern dieser Datenstruktur, sehen wir uns ein Beispiel an:

Anmerkung:Dieser Beispiel ist im Redis-Quellcode erwähnt

//bestehend aus Elementen2,5bestehend aus einer komprimierten Liste
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
 |  |  | | | |
 zlbytes zltail entries "2" "5" end
//Der Inhalt der nach dem Kodieren der Zeichenkette "Hello World"
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]

Oben ist ein Abschnitt, der in Hexadezimal dargestellt wird2,5eine Komprimierte Liste aus zwei Elementen.

  • Zunächst werden die ersten vier Bytes verwendet, um die Anzahl der Bytes der gesamten ziplist anzuzeigen, da Redis kleines Endian verwendet, daher15Ein Byte wird als 0f 00 00 00 dargestellt.
  • Folgendes4Bytes zur Angabe des Offset des letzten Elements, das vom Header (zlbytes) bis zum letzten Element (Hinweis: nicht der Endknoten) beginnt, ebenfalls in Little-Endian-Storage, daher wird es als 0c 00 00 00 angezeigt.
  • Dann folgt das durch zwei Bytes组成的zllen-Feld, das die Anzahl der Elemente darstellt, in unserem Beispiel gibt es zwei Elemente, zusammen mit Little-Endian-Storage, daher wird es als 0 angezeigt2 00.
  • Das nächste ist das Element selbst. Zunächst wird ein variabler Byte-Typ verwendet, um die Länge des vorherigen Elements anzuzeigen2als erster Element, die Größe des vorherigen Elements ist 0, daher wird ein Byte 00 belegt. Gemäß den Kodierungsregeln, die wir oben besprochen haben, wird das Element2und5gehören zu 0~12verwendet werden1111 Zwischen den Zahlen müssen2xxxx-Format codiert,10in Binär ist1ist11heißt1Grund wurde oben erläutert), kombiniert mit2wird als 00 f ausgedrückt3。Gleiches gilt für das Element5ausgedrückt wird, als 02 f6。
  • Die letzte ist der Schluss-Element, der mit einer nicht belegten Kodierung1111 1111Das ist255。

Fügen wir am Ende der komprimierten Liste einen String-Element "Hello World" hinzu. Schauen wir uns zunächst an, wie der String kodiert wird. Gemäß den vereinbarten Kodierungsregeln müssen wir zunächst ein Byte zur Angabe der Länge des vorherigen Elements verwenden. Hier ist das vorherige Element5,insgesamt zwei Bytes belegt, daher wird zunächst ein Byte zur Angabe der Länge des vorherigen Elements 0 verwendet2,nächste ist die Kodierung des Strings. Wir müssen die Länge des Strings hinzufügen, die wir hinzufügen möchten11(Leerzeichen sind auch enthalten),in Binär ist das1011,gemäß den Kodierungsregeln des Strings, verwenden wir 0000 1011Das bedeutet, in Hexadezimal ist es 0b, und schließlich fügen wir den ASCII-Code unseres Strings hinzu. Insgesamt ergibt sich die Kodierung des Strings: [02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]。

Dadurch wird die gesamte komprimierte Liste zu:

[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

Zwei: Analyse des Quellcodes der Befehle der komprimierten Liste ziplist

Nachdem wir die obigen Kodierungsregeln verstanden haben, schauen wir uns die Quellcodes für einige Operationen der komprimierten Liste ziplist an. In diesem Artikel werden wir die grundlegenden Prinzipien der komprimierten Liste anhand der vier Operationen Erstellen, Element hinzufügen, Element löschen und Element suchen zusammenfassen.

Zunächst das Erstellen:

//Definieren Sie die Größe des Headers einer komprimierten Liste, die aus zlbytes, zltail und zllen besteht
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
//Erstellen Sie eine komprimierte Liste und geben Sie den Zeiger auf diese Liste zurück
unsigned char *ziplistNew(void) {
 //Hier liegt der Grund+1weil der Fußknoten einen Byte belegt, das ist auch die minimale Größe einer komprimierten Liste
 unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
 //Speicher zuweisen
 unsigned char *zl = zmalloc(bytes);
 //Die Größe der Liste einstellen
 ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
 //Den Offset des letzten Elements einstellen
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
 //Die Anzahl der Elemente einstellen
 ZIPLIST_LENGTH(zl) = 0;
 //Den letzten Knoten einstellen (das war nur der Raum, der angefordert wurde)
 zl[bytes-1] = ZIP_END;
 return zl;
}

Die Logik zum Erstellen einer komprimierten Liste ist sehr einfach, es geht darum, einen festen Raum für den Kopf- und Fußknoten anzufordern und die Listenkontexte zu initialisieren.

Im Vergleich zum Erstellen ist der Quellcode zum Hinzufügen von Elementen sehr lang, um das Verständnis zu erleichtern, schauen wir uns die Logik des Hinzufügens von Elementen vor dem Betrachten des Quellcodes selbst durch.

  • Zunächst müssen wir die Größe des vorherigen Elements an der angegebenen Einfügeposition finden, da dieses Attribut ein Bestandteil des neuen Elements ist.
  • Dann müssen wir das aktuelle Element kodieren, um das entsprechende encoding-Feld und das Feld des tatsächlichen Elementswerts zu erhalten.
  • Das Nachfolgende Element des neu eingefügten Elements muss das prevlen-Feld aktualisiert werden, da das vordere Element geändert wurde. Dies könnte eine Kaskadenaufgabe verursachen (dieses Problem tritt auch bei der Löschung von Elementen auf), da die Größe des prevlen-Felds variabel ist.

Diese drei Schritte sind die Kernschritte,其余的还有更新尾节点偏移量,修改链表元素个数等操作。Natürlich, da die komprimierte Liste auf einem Array basiert, ist eine Speicherkopie unvermeidlich, wenn Elemente eingefügt oder entfernt werden.

Nachdem die obigen Schritte zusammengefasst sind, beginnen wir mit einer schrittweisen Analyse des Quellcodes, was ziemlich lang ist, also schauen wir uns das langsam an:

//Die vier Parameter sind in dieser Reihenfolge: komprimierte Liste, Einfügeposition (neues Element wird hinter dem Element p eingefügt), Wert des Elements, Länge des Elements
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
 //Hier wird die Länge der aktuellen Liste gespeichert
 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. Das Ziel dieser Logik ist es, die Länge der vorderen Elemente zu ermitteln
 if (p[0] != ZIP_END) {
 //Wenn der Elemente an der Einfügeposition nicht der letzte Element ist, wird die Länge des Elements abgerufen
 //这里为了后面使用方便进行了拆分,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) &63; zipPrevLenByteDiff(p,reqlen) : 0;
 if (nextdiff == -4 && reqlen < 4) {}}
 nextdiff = 0;
 forcelarge = 1;
 }
 //按照新计算出的数组大小进行扩容,由于新数组的地址可能会改变,因此这里记录旧的偏移量
 offset = p-zl;
 zl = ziplistResize(zl,curlen+reqlen+nextdiff);
 //在新数组上计算插入位置
 p = zl+offset;
 //如果新插入的元素不在链表末尾
 if (p[0] != ZIP_END) {
 //将新元素及其后续元素复制到新的数组中,-1用于尾元素
 memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
 //Das prevlen-Feld des nachfolgenden Elements des neuen Elements
 if (forcelarge)
  zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
 else
  zipStorePrevEntryLength(p+reqlen,reqlen);
 //Aktualisieren Sie den Offset des letzten Elements
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
 //Wenn das nächste Element des neuen Elements nicht nur ein Element hat, muss der neue Offset des Endelements um nextdiff hinzugefügt werden
 zipEntry(p+reqlen, &tail);
 if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
  ZIPLIST_TAIL_OFFSET(zl) =
  intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
 }
 } else {
 //Neues Element am Ende der Liste einfügen, aktualisieren Sie den Offset am Ende
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
 }
 //nextdiff != 0 bedeutet, dass die Länge der nachfolgenden Elemente geändert wurde, daher müssen wir die nachfolgenden Elemente der nachfolgenden Elemente kaskadiert aktualisieren
 if (nextdiff != 0) {
 offset = p-zl;
 zl = __ziplistCascadeUpdate(zl,p+reqlen);
 p = zl+offset;
 }
 //Neues Element in die Liste einfügen
 p += zipStorePrevEntryLength(p,prevlen);
 p += zipStoreEntryEncoding(p,encoding,slen);
 if (ZIP_IS_STR(encoding)) {
 memcpy(p,s,slen);
 } else {
 zipSaveInteger(p,value,encoding);
 }
 //Komprimierte Zeilenliste speichert Anzahl der Elemente+1
 ZIPLIST_INCR_LENGTH(zl,1;
 return zl;
}

Nach der Analyse der Logik des Einfügens atmen wir tief durch, es war wirklich lang und es gibt viele Details.

Jetzt schauen wir uns den Prozess der Löschung von Elementen an, im Vergleich zum Hinzufügen ist das Löschen relativ einfach, nach dem das aktuelle Element geleert wird, müssen die nachfolgenden Elemente nacheinander kopiert werden (das ist auch der Unterschied zwischen Array und Zeilenliste als Datenstrukturen), und dann darauf achten, ob eine Kaskadenummerung erforderlich ist, hier der Code:

//Die Parameter sind in der Reihenfolge: Komprimierte Zeilenliste, Anfangsposition des zu löschenden Elements, Anzahl der zu löschenden Elemente
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;
 //Lesen Sie das von p Zeiger referenzierte Element und speichern Sie es in first
 zipEntry(p, &first);
 for (i = 0; p[0] != ZIP_END && i < num; i++) {}}
  //Zähle die gesamte gelöschte Länge
  p += zipRawEntryLength(p);
  //Zähle die Anzahl der tatsächlich gelöschten Elemente
  deleted++;
 }
 //Anzahl der Bytes, die gelöscht werden müssen
 totlen = p-first.p;
 if (totlen > 0) {
  if (p[0] != ZIP_END) {
   //Überprüfen Sie, ob die Größe des Elements geändert wurde
   nextdiff = zipPrevLenByteDiff(p, first.prevrawlen);
   //Ändern Sie das prevlen-Feld des Elements nach dem gelöschten Element
   p -= nextdiff;
   zipStorePrevEntryLength(p, first.prevrawlen);
   //Aktualisieren Sie den Offset des Endelements
   ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
   //Wenn das Nachfolger-Element des gelöschten Elements nicht nur ein Element ist, muss der Offset des neuen Endelements um nextdiff erhöht werden
   zipEntry(p, &tail);
   if (p[tail.headersize+tail.len] != ZIP_END) {
    ZIPLIST_TAIL_OFFSET(zl) =
     intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
   }
   //Bewegen Sie die verbleibenden Elemente nach vorne
   memmove(first.p, p,
    intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1;
  } else {
   //Löschen Sie direkt bis zum Ende der Liste, daher ist keine Speicherkopie erforderlich, nur die Offset des letzten Elements muss geändert werden
   ZIPLIST_TAIL_OFFSET(zl) =
    intrev32ifbe((first.p-zl)-first.prevrawlen);
  }
  //Größe der Array-Größe anpassen
  offset = first.p-zl;
  zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
  //Ändern Sie die Anzahl der Elemente in der Liste
  ZIPLIST_INCR_LENGTH(zl,-deleted);
  p = zl+offset;
  //nextdiff != 0 bedeutet, dass die Größe des Elements geändert wurde und eine Kaskadener更新 erforderlich ist
  if (nextdiff != 0)
   zl = __ziplistCascadeUpdate(zl, p);
 }
 return zl;
}

Schauen wir uns nun das Suchen von Elementen an:

//Die Parameter sind in der folgenden Reihenfolge: Komprimierte Liste, Wert des zu suchenden Elements, Länge des zu suchenden Elements, Anzahl der Elemente, die zwischen den Vergleichen übersprungen werden
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 对内存利用率如此之高,那为什么还要提供普通的链表结构供用户使用呢?
这个问题没有标准答案,仁者见仁,智者见智。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或工作具有一定的参考价值。如果有疑问,大家可以留言交流,感谢大家对呐喊教程的支持。

声明:本文内容来自网络,版权归原作者所有。内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#example.com。3codebox.com(在发邮件时,请将#更换为@)进行举报,并提供相关证据。一经查实,本站将立即删除涉嫌侵权内容。

Gefällt mir