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

Análisis de la fuente de redis: detalles de la lista comprimida ziplist

Prólogo

La lista comprimida (ziplist) está compuesta por una serie de bloques de memoria codificados de manera especial, lo que tiene un papel muy importante en la optimización de almacenamiento de datos de Redis. Este artículo resume uno de los datos estructurales más utilizados en Redis, la lista enlazada comprimida ziplist. Dicho sea de paso, no es exagerado decir que esta estructura de datos está en todas partes en Redis, además de la lista, muchas otras estructuras de datos también la utilizan como transición, como se mencionó en el artículo anterior, SortedSet. Sin más preámbulos, veamos una introducción detallada.

I. Introducción a la estructura de datos de lista enlazada comprimida ziplist

Primero, observemos la estructura general de ziplist, como se muestra en la siguiente imagen:

Esquema de estructura de ziplist

Se puede ver que hay muchos campos y diferentes tamaños de bytes, pero esto es esencialmente la esencia de la lista enlazada comprimida, resumamos en sucesión.

 

campo significado
zlbytes Este campo es el primer campo de la lista enlazada comprimida, es un entero sin signo, ocupa4bytes. Se utiliza para representar el número total de bytes ocupados por la lista enlazada comprimida (incluso a sí misma).
zltail Entero sin signo, ocupa4bytes. Se utiliza para almacenar la cantidad de desplazamiento desde la cabeza de la lista enlazada comprimida hasta el último entry (no el elemento final zlend), utilizado en escenarios de saltos rápidos al final de la lista.
zllen Entero sin signo, ocupa2bytes. Se utiliza para almacenar el número total de entry que contiene la lista enlazada comprimida.
zlend Entry especial utilizado para representar el final de la lista enlazada comprimida. Ocupa un byte y el valor es constante de255。

Resumiendo, la cabeza y el final de ziplist, a continuación, hablemos brevemente de los campos más importantes de entry.

En general, un entry consta de prevlen, encoding y entry-data三个字段组成,但当entry是个很小的整数时,会根据编码省略掉entry-los campos data, prevlen y entry forman, pero cuando el entry es un número muy pequeño, se omiten según la codificación.

campo data. A continuación, se realiza una resumen:

  • Primero es el campo prevlen:representa la longitud del anterior entry, hay dos métodos de codificación.255cuando la longitud es menor
  • bytes, se almacena con un byte.255cuando la longitud es mayor o igual a255se representa que la longitud del anterior entry se indica por los cuatro bytes posteriores.

Luego es el campo encoding:dependiendo del contenido del elemento actual, adopta diferentes métodos de codificación, como se muestra a continuación:

1、si el contenido del elemento es una cadena de caracteres, los valores de encoding son:

00xx xxxx :00 al principio indica que la longitud de la cadena de caracteres se utiliza6bits representan.

01xx xxxx | xxxx xxxx :01El principio indica que la longitud de la cadena de caracteres se14bits representan, esto14bits se almacenan en orden mayor.

1000 0000 | xxxx xxxx | xxxx xxxx | xxxx xxxx | xxxx xxxx :10El principio indica que los cuatro bytes posteriores son la longitud de la cadena de caracteres, esto32bits se almacenan en orden mayor.

2、si el contenido del elemento es un número, los valores de encoding son:

1100 0000:representa que el número ocupa los bytes posteriores2bytes.

1101 0000:representa que el número ocupa los bytes posteriores4bytes.

1110 0000:representa que el número ocupa los bytes posteriores8bytes.

1111 0000 :representa que el número ocupa los bytes posteriores3bytes.

1111 1110 :representa que el número ocupa los bytes posteriores1bytes.

1111 1111 :representa el último elemento de la lista enlazada comprimida (codificación especial).

1111 xxxx :representa que solo se utilizan los bits posteriores4bits representan 0~12enteros, ya que 0000,1110como el primer elemento, el tamaño del elemento anterior es 0, por lo que ocupa un byte 00. Según las reglas de codificación que mencionamos anteriormente, el elemento1111tres ya están ocupados, lo que significa que aquí los cuatro dígitos xxxx solo pueden representar 0001~1101convertido a decimal es el número1~13pero Redis regula que se utiliza para representar 0~12,por lo que cuando se encuentra con esta codificación, es necesario extraer las últimas cuatro cifras y restar1para obtener el valor correcto.

último es el campo entry-data:si el valor del elemento es una cadena de caracteres, se guarda el valor del elemento en sí. Si el valor del elemento es un número muy pequeño (según la regla de codificación anterior, es 0~12),no hay ese campo.

La codificación de la lista enlazada comprimida es muy compleja, pero también es la esencia de esta estructura de datos, veamos un ejemplo:

Nota:Este ejemplo es mencionado en el código fuente de Redis

//constituida por los elementos2,5formada por la lista enlazada comprimida
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
 |  |  | | | |
 zlbytes zltail entries "2" "5" end
//El contenido codificado de la cadena de caracteres "Hello World"
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]

La parte superior es un fragmento representado en hexadecimal2,5una lista enlazada comprimida formada por dos elementos.

  • Primero, los cuatro primeros bytes representan el número total de bytes ocupados por la ziplist, ya que Redis utiliza almacenamiento en orden menor, por lo que15Un byte se representa como 0f 00 00 00.
  • 接下来的4siguientes
  • bytes para representar la offset del elemento final, es desde la cabeza (zlbytes) hasta el último elemento (nota: no es el nodo final) de la lista enlazada, también se utiliza el almacenamiento en pequeño, por lo que se representa como 0c 00 00 00.2 después de eso, hay un campo zllen de dos bytes que representa el número de elementos, en nuestro ejemplo hay dos elementos, más el almacenamiento en pequeño, por lo que se representa como 0
  • 00.2a continuación, es el elemento en sí mismo, primero se utiliza un entero de longitud variable para representar la longitud del elemento anterior,2como el primer elemento, el tamaño del elemento anterior es 0, por lo que ocupa un byte 00. Según las reglas de codificación que mencionamos anteriormente, el elemento5y12que pertenece a 0~1111 entre los números, se necesita usar2xxxx formato para la codificación,10convertido en binario es 001sumado11la razón de lo cual se ha explicado anteriormente), combinando los elementos1se representa como 00 f2al igual que el elemento3。5representada como 02 f6。
  • por último, es el elemento final, se utiliza la codificación no ocupada1111 1111es decir255。

a continuación, insertamos un elemento de cadena de caracteres Hello World en la cola de esta lista enlazada comprimida, primero veamos cómo codificar la cadena de caracteres, según las reglas de codificación acordadas, primero se utiliza un byte para representar la longitud del elemento anterior, aquí el elemento anterior es5ocupa dos bytes en total, por lo que primero se utiliza un byte para representar la longitud del elemento anterior 02a continuación, es la codificación de la cadena de caracteres, la longitud de la cadena de caracteres que vamos a agregar es11el espacio en blanco también se considera), convertido en binario es1011de acuerdo con las reglas de codificación de la cadena de caracteres, utilizando 0000 1011lo que representa, convertido en hexadecimal es 0b, y luego se agrega el código ASCII de la cadena de caracteres en sí, combinándolos se obtiene la codificación de la cadena de caracteres: [02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]。

En este momento, toda la lista enlazada comprimida se convierte en:

[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

Segundo, análisis del código fuente del comando de lista enlazada comprimida ziplist

Después de entender las reglas de codificación anteriores, veamos algunas operaciones del código fuente de la lista enlazada comprimida ziplist. En este artículo, se resume el principio básico de la lista enlazada comprimida a través de las operaciones de creación, inserción, eliminación y búsqueda de elementos.

Primero es la creación:

//Definir el tamaño de la cabecera de la lista enlazada comprimida compuesta por zlbytes, zltail y zllen
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
//Crear una lista enlazada comprimida y devolver el puntero a la lista
unsigned char *ziplistNew(void) {
 //la razón por la que aquí es+1debido a que el elemento final ocupa un byte, esto es también el tamaño mínimo de una lista comprimida
 unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
 //Asignar memoria
 unsigned char *zl = zmalloc(bytes);
 //Establecer el tamaño de la lista
 ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
 //Establecer el desplazamiento del último elemento
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
 //Establecer el número de elementos
 ZIPLIST_LENGTH(zl) = 0;
 //Establecer el elemento final (solo se solicitó espacio anteriormente)
 zl[bytes-1]= ZIP_END;
 return zl;
}

La lógica de creación de la lista comprimida es muy simple, es decir, solicitar un espacio fijo que contenga los nodos de encabezado y final, y luego inicializar el contexto de la lista.

En comparación con la creación, el código fuente de la adición de elementos es muy largo, para facilitar la comprensión, antes de ver el código, primero ordenamos lógicamente la lógica de adición de elementos.

  • Primero debemos encontrar el tamaño del elemento anterior a la posición de inserción especificada, ya que este atributo es una parte del elemento nuevo.
  • Luego debemos codificar el elemento actual para obtener los campos correspondientes a encoding y al valor real del elemento.
  • El campo prevlen del elemento siguiente al elemento insertado nuevo debe ser actualizado, ya que el elemento anterior ha cambiado. Esto puede causar actualizaciones en cadena (también hay este problema al eliminar elementos), la razón es que el tamaño del campo prevlen es variable.

Estos tres pasos son las etapas clave, y las otras incluyen operaciones como actualizar el desplazamiento del nodo final, modificar el número de elementos de la lista, etc. Por supuesto, debido a que la lista comprimida se implementa basada en un array, la copia de memoria también es indispensable en el momento de la inserción o eliminación de elementos.

Después de resumir los pasos anteriores, comenzamos a analizar paso a paso el código fuente, es largo, veamos lentamente:

//Los cuatro parámetros en sucesión son: lista comprimida, posición de inserción (nuevo elemento insertado después del elemento p), valor del elemento, longitud del elemento
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
 //Aquí se guarda la longitud actual de la lista enlazada
 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. El propósito de esta lógica es obtener la longitud del elemento anterior
 if (p[0] != ZIP_END) {
 //Si el elemento en la posición de inserción no es el elemento final, obtener la longitud del elemento
 //aquí se ha dividido para facilitar su uso posterior, prevlensize guarda la longitud del campo encoding, prevlen guarda la longitud del elemento en sí
 ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
 } else {
 //si el elemento en la posición de inserción es el elemento final, entonces es necesario insertar el nuevo elemento en el extremo de la lista
 //obtener el último elemento de la lista (nota: el último elemento no es el elemento final)
 unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
 if (ptail[0] != ZIP_END) {
  //si el último elemento no es el elemento final, entonces este elemento es el elemento previo del nuevo elemento, obtener la longitud de este elemento
  prevlen = zipRawEntryLength(ptail);
 }
 //de lo contrario, significa que la lista aún no tiene ningún elemento, es decir, la longitud del elemento previo del nuevo elemento es 0
 }
 //2. codificar el nuevo elemento, obtener el tamaño total del nuevo elemento
 if (zipTryEncoding(s,slen,&value,&encoding)) {
 //si es un número, se codifica según el número
 reqlen = zipIntSize(encoding);
 } else {
 //la longitud del elemento es la longitud de la cadena
 reqlen = slen;
 }
 //la longitud total del nuevo elemento es la longitud del valor más la longitud del elemento previo y la longitud del encoding
 reqlen += zipStorePrevEntryLength(NULL,prevlen);
 reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
 //si la posición de inserción no es la parte final de la lista enlazada,则需要判断新元素的后续元素的prevlen campo
 //según las reglas de codificación anteriores, este campo puede necesitar expandirse
 int forcelarge = 0;
 nextdiff = (p[0] != ZIP_END) &63; zipPrevLenByteDiff(p,reqlen) : 0;
 if (nextdiff == -4 && reqlen < 4) {}}
 nextdiff = 0;
 forcelarge = 1;
 }
 //expandir según el nuevo tamaño del array calculado, ya que la dirección del nuevo array puede cambiar, por lo tanto, aquí se registra el desplazamiento antiguo
 offset = p-zl;
 zl = ziplistResize(zl,curlen+reqlen+nextdiff);
 //calcular la posición de inserción en el nuevo array
 p = zl+offset;
 //si el elemento insertado no está en la parte final de la lista enlazada
 if (p[0] != ZIP_END) {
 //copiar el elemento siguiente al nuevo elemento al nuevo array,-1para el elemento final
 memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
 //El campo prevlen del elemento siguiente del nuevo elemento
 if (forcelarge)
  zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
 else
  zipStorePrevEntryLength(p+reqlen,reqlen);
 //Actualizar el desplazamiento del último elemento
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
 //Cuando el elemento siguiente del nuevo elemento no tiene más de un elemento, el nuevo desplazamiento del elemento final debe sumar nextdiff
 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 {
 //Insertar el nuevo elemento al final de la lista y actualizar el desplazamiento del final
 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
 }
 //nextdiff != 0 indica que el tamaño del elemento siguiente ha cambiado, por lo tanto, necesitamos actualizar en cadena los elementos siguientes del elemento siguiente
 if (nextdiff != 0) {
 offset = p-zl;
 zl = __ziplistCascadeUpdate(zl,p+reqlen);
 p = zl+offset;
 }
 //Escribir el nuevo elemento en la lista
 p += zipStorePrevEntryLength(p,prevlen);
 p += zipStoreEntryEncoding(p,encoding,slen);
 if (ZIP_IS_STR(encoding)) {
 memcpy(p,s,slen);
 } else {
 zipSaveInteger(p,value,encoding);
 }
 //El almacenamiento de la lista de compactación almacena la cantidad de elementos+1
 ZIPLIST_INCR_LENGTH(zl,1;
 return zl;
}

Después de analizar la lógica de inserción de elementos, suspiro aliviado, realmente es largo, y hay muchos detalles.

Después de analizar el proceso de eliminación de elementos, suspiro aliviado, realmente es largo, y hay muchos detalles.

//Los parámetros son: la lista de compactación, la posición inicial del elemento a eliminar, y la cantidad de elementos a eliminar
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;
 //Leer el elemento apuntado por p y guardarlo en first
 zipEntry(p, &first);
 for (i = 0; p[0] != ZIP_END && i < num; i++) {}}
  //contar la longitud total eliminada
  p += zipRawEntryLength(p);
  //contar el número real de elementos eliminados
  deleted++;
 }
 //número de bytes a eliminar del elemento
 totlen = p-first.p;
 if (totlen > 0) {
  if (p[0] != ZIP_END) {
   //juzgar si el tamaño del elemento ha cambiado
   nextdiff = zipPrevLenByteDiff(p, first.prevrawlen);
   //modificar el campo prevlen del elemento eliminado
   p -= nextdiff;
   zipStorePrevEntryLength(p, first.prevrawlen);
   //actualizar el desplazamiento del elemento final
   ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
   //cuando el elemento siguiente a eliminar tiene más de un elemento, se debe agregar nextdiff al desplazamiento del nuevo elemento final
   zipEntry(p, &tail);
   if (p[tail.headersize+tail.len] != ZIP_END) {
    ZIPLIST_TAIL_OFFSET(zl) =
     intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
   }
   //mover los elementos restantes detrás al frente
   memmove(first.p, p,
    intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1;
  } else {
   //eliminar directamente hasta el final de la lista, por lo que no se requiere copia de memoria, solo se debe modificar el desplazamiento del último elemento
   ZIPLIST_TAIL_OFFSET(zl) =
    intrev32ifbe((first.p-zl)-first.prevrawlen);
  }
  //redimensionar el tamaño del array
  offset = first.p-zl;
  zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
  //modificar el número de elementos en la lista
  ZIPLIST_INCR_LENGTH(zl,-deleted);
  p = zl+offset;
  //nextdiff != 0 indica que el tamaño del elemento ha cambiado, por lo que se requiere una actualización en cascada
  if (nextdiff != 0)
   zl = __ziplistCascadeUpdate(zl, p);
 }
 return zl;
}

Veamos la operación de búsqueda del elemento al final:

//Los parámetros son: lista enlazada comprimida, valor del elemento a buscar, longitud del elemento a buscar, número de elementos saltados entre comparaciones
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) {
      //Si dos números son iguales, devolver el puntero del elemento
      return p;
     }
    }
   }
   //Reiniciar la cantidad de elementos que se deben saltar
   skipcnt = skip;
  } else {
   //Saltar elementos
   skipcnt--;
  }
  //Recorrer el siguiente elemento
  p = q + len;
 }
 //Se ha recorrido toda la lista sin encontrar el elemento
 return NULL;
}

Hasta aquí, se han resumido los principios de las cuatro operaciones básicas de creación, adición, eliminación y búsqueda de la lista comprimida.

Tercero, resumen de la estructura de datos de lista comprimida ziplist

La aplicación de la compresión de lista ziplist en redis es muy amplia, es una de las estructuras de datos más características de redis. La esencia de esta estructura de datos realmente radica en las reglas de codificación resumidas en la primera parte del artículo. Después de entender este contenido, puede profundizar en la comprensión del código posterior.

Hay que decir que la parte del código es realmente larga, realmente necesita paciencia, incluso estaba muy confundido mientras leía. Si están interesados en el código, sugiero seguir mi método, primero organizar por sí mismos qué debe hacer una operación específica (por ejemplo, la inserción de elementos mencionada anteriormente), y luego ver el código puede ser más fácil de entender.

Dejamos un pequeño problema al final, si el ziplist de redis tiene una alta utilización de memoria, ¿por qué aún se proporciona la estructura de lista común para su uso?
No hay respuesta estándar a esta pregunta, cada persona tiene su propia interpretación.

Resumen

Eso es todo el contenido de este artículo. Espero que el contenido de este artículo tenga un valor de referencia para su aprendizaje o trabajo. Si tienen alguna pregunta, pueden dejar comentarios para intercambiar. Gracias por su apoyo a la tutorial de gritos.

Declaración: El contenido de este artículo se ha obtenido de la red, es propiedad del autor original, el contenido se ha contribuido y subido por los usuarios de Internet de manera autónoma. Este sitio no posee los derechos de propiedad, no ha sido editado por humanos y no asume ninguna responsabilidad legal relacionada. Si encuentra contenido sospechoso de copyright, por favor envíe un correo electrónico a: notice#oldtoolbag.com (al enviar un correo electrónico, reemplace # con @) para denunciar y proporcionar evidencia relevante. Una vez confirmado, este sitio eliminará inmediatamente el contenido sospechoso de infracción.

Te gustará