Estudio en profundidad de Redis: modelo de memoria de Redis

1. Estadísticas de memoria Redis

Si quiere hacer un buen trabajo, primero debe afilar sus herramientas.Antes de explicar la memoria de Redis, primero explique cómo contar el uso de memoria de Redis.

Después de que el cliente se conecte al servidor a través de redis-cli (si no hay una instrucción especial a continuación, el cliente usará redis-cli), puede verificar el uso de la memoria a través del comando info:

1

info memory

Entre ellos, el comando info puede mostrar mucha información sobre el servidor redis, incluida información básica del servidor, CPU, memoria, persistencia, información de conexión del cliente, etc.; la memoria es un parámetro que indica que solo se muestra información relacionada con la memoria.

Varias instrucciones importantes en los resultados devueltos son las siguientes:

(1) used_memory : la cantidad total de memoria asignada por el asignador de Redis (en bytes), incluida la memoria virtual utilizada (es decir, intercambio); el asignador de Redis se presentará más adelante. used_memory_human simplemente se muestra más amigable.

(2) used_memory_rss : El proceso de Redis ocupa la memoria del sistema operativo (en bytes), lo cual es consistente con los valores vistos por los comandos top y ps; además de la memoria asignada por el asignador, used_memory_rss también incluye el memoria requerida para que el proceso se ejecute solo, fragmentación de memoria, etc., pero no incluye memoria virtual.

Por lo tanto used_memory y used_memory_rss, la primera es la cantidad obtenida desde la perspectiva de Redis, y la segunda es la cantidad obtenida desde la perspectiva del sistema operativo. La razón de la diferencia entre los dos es que, por un lado, la fragmentación de la memoria y la memoria ocupada por el proceso de Redis pueden hacer que el primero sea más pequeño que el segundo y, por otro lado, la existencia de memoria virtual puede hacer que el primero sea más grande. que este último.

Dado que la cantidad de datos en Redis será relativamente grande en las aplicaciones prácticas, la memoria ocupada por el proceso que se ejecuta en este momento será mucho menor que la cantidad de datos de Redis y la fragmentación de la memoria; por lo tanto, la proporción de used_memory_rss y used_memory se convierte en una medida de la fragmentación de la memoria de Redis El parámetro de la tasa; este parámetro es mem_fragmentation_ratio.

(3) mem_fragmentation_ratio : relación de fragmentación de memoria, este valor es la relación de used_memory_rss / used_memory.

mem_fragmentation_ratio es generalmente mayor que 1, y cuanto mayor sea el valor, mayor será la tasa de fragmentación de la memoria. mem_fragmentation_ratio<1, lo que indica que Redis usa memoria virtual. Dado que el medio de la memoria virtual es el disco, es mucho más lento que la memoria. Cuando esto sucede, debe verificarse a tiempo. Si la memoria es insuficiente, debe tratarse en tiempo, como agregar nodos Redis, memoria del servidor Redis, aplicaciones optimizadas, etc.

En términos generales, mem_fragmentation_ratio se encuentra en un estado relativamente saludable alrededor de 1.03 (para jemalloc); el valor de mem_fragmentation_ratio en la captura de pantalla anterior es muy grande, porque los datos no se han almacenado en Redis, y la memoria del propio proceso de Redis hace used_memory_rss que used_memory Mucho más grande.

(4) mem_allocator : el asignador de memoria utilizado por Redis, que se especifica en el momento de la compilación; puede ser libc, jemalloc o tcmalloc, y el predeterminado es jemalloc; el predeterminado jemalloc se usa en la captura de pantalla.

2. División de memoria Redis

Como base de datos en memoria, Redis almacena principalmente datos (pares clave-valor); de la descripción anterior, podemos saber que además de los datos, otras partes de Redis también ocupan memoria.

El uso de memoria de Redis se puede dividir en las siguientes partes:

1. Datos

Como base de datos, los datos son la parte más importante; la memoria ocupada por esta parte se contará en used_memory.

Redis utiliza pares clave-valor para almacenar datos, y los valores (objetos) incluyen cinco tipos, a saber, cadenas, hashes, listas, conjuntos y conjuntos ordenados. Estos 5 tipos son proporcionados por Redis. De hecho, dentro de Redis, cada tipo puede tener 2 o más implementaciones de codificación interna; además, cuando Redis almacena objetos, no arroja datos directamente a la memoria, pero los objetos se empaquetarán de varias maneras. : como redisObject, SDS, etc.; más adelante en este artículo, nos centraremos en los detalles del almacenamiento de datos en Redis.

2. La memoria requerida por el propio proceso para ejecutarse

La operación del proceso principal de Redis en sí debe ocupar memoria, como código, grupo constante, etc.; esta parte de la memoria es de unos pocos megabytes, que se pueden ignorar en comparación con la memoria que ocupan los datos de Redis en la mayoría de los entornos de producción. Jemalloc no asigna esta parte de la memoria, por lo que no se contará en used_memory.

Nota complementaria: además del proceso principal, el subproceso creado por Redis también ocupará memoria, como el subproceso creado cuando Redis ejecuta la reescritura de AOF y RDB. Por supuesto, esta parte de la memoria no pertenece al proceso de Redis, ni se contará en used_memory y used_memory_rss.

3. Memoria intermedia

La memoria de búfer incluye el búfer del cliente, el búfer de copias pendientes, el búfer AOF, etc. Al escribir, guarde el comando de escritura más reciente. No es necesario conocer los detalles de estos búferes antes de comprender las funciones correspondientes; esta parte de la memoria la asigna jemalloc, por lo que se contará en used_memory.

4. Fragmentación de la memoria

La fragmentación de la memoria se genera cuando Redis asigna y reclama memoria física. Por ejemplo, si los datos se cambian con frecuencia y el tamaño de los datos difiere mucho, es posible que el espacio liberado por redis no se libere en la memoria física, pero redis no se puede usar de manera efectiva, lo que forma la fragmentación de la memoria. La fragmentación de la memoria no se contará en used_memory.

La generación de fragmentación de memoria está relacionada con el funcionamiento de los datos, las características de los datos, etc.; además, también está relacionada con el asignador de memoria utilizado: si el asignador de memoria está diseñado correctamente, la generación de fragmentación de memoria puede reducirse lo más posible. Jemalloc, que se mencionará más adelante, hace un buen trabajo al controlar la fragmentación de la memoria.

Si la fragmentación de la memoria en el servidor Redis ya es grande, puede reducir la fragmentación de la memoria reiniciando de manera segura: porque después de reiniciar, Redis lee los datos del archivo de respaldo nuevamente, los reorganiza en la memoria y vuelve a seleccionar los datos apropiados para cada dato. unidad de memoria para reducir la fragmentación de la memoria.

3. Detalles del almacenamiento de datos de Redis

1. Información general

Los detalles sobre el almacenamiento de datos de Redis involucran el asignador de memoria (como jemalloc), cadena dinámica simple (SDS), 5 tipos de objetos y codificación interna, redisObject. Antes de describir el contenido específico, primero explique la relación entre estos conceptos.

La siguiente figura es el modelo de datos involucrado al ejecutar set hello world.

Fuente de la imagen: https://searchdatabase.techtarget.com.cn/7-20218/

(1) dictEntry: Redis es una base de datos de clave-valor, por lo que habrá una dictEntry para cada par clave-valor, que almacena punteros a clave y valor; next apunta a la siguiente dictEntry, que no tiene nada que ver con esta clave-valor. Valor.

(2) Clave: se puede ver en la esquina superior derecha de la figura que la clave ("hola") no se almacena directamente como una cadena, sino que se almacena en la estructura SDS.

(3) redisObject: Value("world") no se almacena directamente como una cadena, ni se almacena directamente en SDS como Key, sino que se almacena en redisObject. De hecho, no importa cuál de los cinco tipos sea Valor, se almacena a través de redisObject; y el campo tipo en redisObject indica el tipo de objeto Valor, y el campo ptr apunta a la dirección donde se encuentra el objeto. Sin embargo, se puede ver que aunque el objeto de cadena ha sido empaquetado por redisObject, SDS aún debe almacenarlo.

De hecho, además de los campos type y ptr, redisObject tiene otros campos que no se muestran en la figura, como el campo utilizado para especificar la codificación interna del objeto, que se describirá en detalle más adelante.

(4) jemalloc: ya sea un objeto DictEntry, un objeto redisObject o SDS, se requiere un asignador de memoria (como jemalloc) para asignar memoria para el almacenamiento. Tomando el objeto DictEntry como ejemplo, consta de 3 punteros, ocupando 24 bytes en una máquina de 64 bits, y jemalloc le asignará una unidad de memoria de 32 bytes.

Presentemos jemalloc, redisObject, SDS, tipo de objeto y codificación interna respectivamente.

2, jemalloc

Redis especificará el asignador de memoria al compilar; el asignador de memoria puede ser libc, jemalloc o tcmalloc, y el predeterminado es jemalloc.

Como asignador de memoria predeterminado de Redis, jemalloc hace un trabajo relativamente bueno en la reducción de la fragmentación de la memoria. En un sistema de 64 bits, jemalloc divide el espacio de memoria en tres rangos: pequeño, grande y enorme; cada rango se divide en muchas unidades de bloque de memoria pequeñas; cuando Redis almacena datos, seleccionará el bloque de memoria con el tamaño más apropiado para procesamiento almacenamiento.

La unidad de memoria dividida por jemalloc se muestra en la siguiente figura:

Por ejemplo, si se necesita almacenar un objeto de 130 bytes de tamaño, jemalloc lo colocará en una unidad de memoria de 160 bytes.

3, objeto redis

Como se mencionó anteriormente, hay 5 tipos de objetos Redis; sin importar el tipo, Redis no los almacenará directamente, sino que los almacenará a través del objeto redisObject.

El objeto redisObject es muy importante. El tipo de objeto Redis, la codificación interna, la recuperación de memoria, los objetos compartidos y otras funciones necesitan el soporte de redisObject. A continuación se explicará cómo funciona a través de la estructura de redisObject.

La definición de redisObject es la siguiente (diferentes versiones de Redis pueden ser ligeramente diferentes):

1

2

3

4

5

6

7

typedef struct redisObject {

  unsigned type:4;

  unsigned encoding:4;

  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

  int refcount;

  void *ptr;

} robj;

El significado y la función de cada campo de redisObject son los siguientes:

(1) tipo

El campo de tipo indica el tipo de objeto, representando 4 bits; actualmente incluye REDIS_STRING (cadena), REDIS_LIST (lista), REDIS_HASH (hash), REDIS_SET (conjunto), REDIS_ZSET (conjunto ordenado).

Cuando ejecutamos el comando type, obtenemos el tipo del objeto leyendo el campo type de RedisObject; como se muestra en la siguiente figura:

(2) codificación

la codificación representa la codificación interna del objeto, que representa 4 bits.

Para cada tipo compatible con Redis, hay al menos dos codificaciones internas. Por ejemplo, para las cadenas, hay tres codificaciones: int, embstr y raw. A través del atributo de codificación, Redis puede establecer diferentes codificaciones para objetos de acuerdo con diferentes escenarios de uso, lo que mejora en gran medida la flexibilidad y la eficiencia de Redis. Tomando el objeto de lista como ejemplo, hay dos métodos de codificación: lista comprimida y lista enlazada de dos extremos; si hay menos elementos en la lista, Redis tiende a usar la lista comprimida para el almacenamiento, porque la lista comprimida ocupa menos memoria y puede ser más eficiente que una lista enlazada de dos extremos Carga rápida: cuando el objeto de lista tiene muchos elementos, la lista comprimida se transformará en una lista enlazada de dos extremos que es más adecuada para almacenar una gran cantidad de elementos.

A través del comando de codificación de objetos, puede ver el método de codificación adoptado por el objeto, como se muestra en la siguiente figura:

Los métodos de codificación y las condiciones de uso correspondientes a los cinco tipos de objetos se presentarán más adelante.

(3) lru

lru registra la hora en que el programa de comando accedió por última vez al objeto, y la cantidad de bits ocupados por diferentes versiones es diferente (por ejemplo, la versión 4.0 ocupa 24 bits y la versión 2.6 ocupa 22 bits).

Al comparar el tiempo lru con el tiempo actual, puede calcular el tiempo de inactividad de un objeto; el comando object idletime puede mostrar el tiempo de inactividad (en segundos). Una característica especial del comando object idletime es que no cambia el valor lru del objeto.

Además de imprimir el valor lru a través del comando object idletime, también está relacionado con la recuperación de memoria de Redis: si la opción maxmemory está habilitada en Redis y el algoritmo de recuperación de memoria es volatile-lru o allkeys—lru, entonces cuando el el uso de memoria de Redis supera el valor especificado por maxmemory Cuando el valor se establece en , Redis dará prioridad al objeto con el tiempo de inactividad más prolongado para su liberación.

(4) refcount

refcount y objetos compartidos

refcount registra el número de veces que se hace referencia al objeto, el tipo es entero y ocupa 4 bytes. El rol de refcount es principalmente en el conteo de referencias de objetos y la recuperación de memoria. Cuando se crea un nuevo objeto, refcount se inicializa en 1; cuando un nuevo programa usa el objeto, refcount se incrementa en 1; cuando el objeto ya no es usado por un nuevo programa, refcount se reduce en 1; cuando refcount se convierte en 0, la memoria ocupada por el objeto será liberada.

Los objetos (refcount>1) que se usan varias veces en Redis se denominan objetos compartidos. Para ahorrar memoria, cuando algunos objetos aparecen repetidamente, el nuevo programa no creará nuevos objetos, sino que seguirá utilizando los objetos originales. Este objeto reutilizado es un objeto compartido. Los objetos compartidos actualmente solo admiten objetos de cadena con valores enteros.

Implementación concreta de objetos compartidos

Los objetos compartidos de Redis actualmente solo admiten objetos de cadena con valores enteros. La razón de esto es en realidad un equilibrio entre la memoria y la CPU (tiempo): aunque compartir objetos reducirá el consumo de memoria, se necesita más tiempo para determinar si dos objetos son iguales. Para valores enteros, la complejidad de la operación de juicio es O(1); para cadenas ordinarias, la complejidad del juicio es O(n); y para hashes, listas, conjuntos y conjuntos ordenados, la complejidad del juicio es O(n^ 2).

Aunque los objetos compartidos solo pueden ser objetos de cadena con valores enteros, los 5 tipos pueden usar objetos compartidos (se pueden usar elementos como hashes, listas, etc.).

En lo que respecta a la implementación actual, cuando se inicializa el servidor Redis, creará 10.000 objetos de cadena cuyos valores son valores enteros de 0 a 9999; cuando Redis necesita usar objetos de cadena con valores de 0 a 9999, puede usar directamente estos compartidos con. El número 10000 se puede cambiar ajustando el valor del parámetro REDIS_SHARED_INTEGERS (OBJ_SHARED_INTEGERS en 4.0).

El número de referencias a objetos compartidos se puede ver a través del comando object refcount, como se muestra en la figura a continuación. La página de resultados de la ejecución del comando demuestra que solo los números enteros entre 0 y 9999 se utilizarán como objetos compartidos.

(5) puntos

El puntero ptr apunta a datos específicos, como en el ejemplo anterior, set hello world, ptr apunta al SDS que contiene la cadena world. La cantidad de bytes que ocupa el puntero ptr está relacionada con el sistema, por ejemplo, ocupa 8 bytes en un sistema de 64 bits.

(6) Resumen

En resumen, la estructura de redisObject está relacionada con el tipo de objeto, la codificación, la recuperación de memoria y los objetos compartidos; en un sistema de 64 bits, el tamaño de un objeto redisObject es de 16 bytes:

4bit+4bit+24bit+4Byte+8Byte=16Byte。

4, SDS

Redis no utiliza directamente cadenas C (es decir, matrices de caracteres terminadas en el carácter nulo '\0') como representación de cadena predeterminada, sino que utiliza SDS. SDS es la abreviatura de Simple Dynamic String.

(1) estructura SDS

La estructura de sds es la siguiente:

1

2

3

4

5

struct sdshdr {

    int len;

    int free;

    char buf[];

};

Entre ellos, buf representa una matriz de bytes utilizada para almacenar cadenas; len representa la longitud de buf utilizada y free representa la longitud de buf que no se utiliza. A continuación se muestran dos ejemplos.

Fuente de la imagen: "Diseño e implementación de Redis"

Se puede ver en la estructura de SDS que la longitud de la matriz buf = libre + len + 1 (donde 1 representa el carácter nulo al final de la cadena); por lo tanto, el espacio ocupado por una estructura SDS es: la longitud ocupado por free + la longitud ocupada por len + La longitud de la matriz buf=4+4+free+len+1=free+len+9.

(2) Comparación de cadenas SDS y C

SDS agrega campos libres y largos sobre la base de cadenas C, lo que brinda muchos beneficios:

  • Obtenga la longitud de la cadena: SDS es O (1), la cadena C es O (n)
  • Desbordamiento de búfer: al usar la API de cadena C, si la longitud de la cadena aumenta (como la operación strcat) y olvida reasignar la memoria, es fácil causar un desbordamiento de búfer; y SDS registra la longitud, la API correspondiente puede causar almacenamiento en búfer cuando el área se desborda, reasignará automáticamente la memoria para evitar el desbordamiento del búfer.
  • Reasignación de memoria al modificar cadenas: Para cadenas C, si desea modificar la cadena, debe reasignar la memoria (liberar primero y luego aplicar), porque si no hay reasignación, el búfer de memoria se desbordará cuando aumente la longitud de la cadena, cuando la longitud de la cadena se reduce, provocará una pérdida de memoria. Para SDS, dado que se pueden registrar len y free, se libera la asociación entre la longitud de la cadena y la longitud de la matriz de espacio, y la optimización se puede realizar sobre esta base: estrategia de preasignación de espacio (es decir, se necesita más memoria asignado de lo que realmente se necesita) hace que la probabilidad de reasignar memoria se reduzca en gran medida cuando la longitud de la cadena aumenta; la estrategia de liberación de espacio diferido reduce en gran medida la probabilidad de reasignar memoria cuando la longitud de la cadena disminuye.
  • Acceso a datos binarios: SDS puede, cadenas C no. Debido a que la cadena C usa un carácter nulo como final de la cadena, y para algunos archivos binarios (como imágenes, etc.), el contenido puede incluir una cadena vacía, por lo que no se puede acceder correctamente a la cadena C; y SDS usa la longitud de la cadena len como el indicador de fin de cadena, por lo que no hay tal problema.

Además, dado que buf en SDS todavía usa cadenas C (es decir, termina con '\0'), SDS puede usar algunas funciones en la biblioteca de cadenas C; pero debe tenerse en cuenta que solo cuando SDS se usa para almacenar datos de texto. solo se puede usar cuando se almacenan datos binarios ('\0' no es necesariamente el final).

(3) Aplicación de SDS y cadena C

Cuando Redis almacena objetos, siempre usa SDS en lugar de cadenas C. Por ejemplo, el comando set hola mundo, hola y mundo se almacenan en forma de SDS. El comando sadd myset miembro1 miembro2 miembro3, ya sea la clave ("miconjunto") o los elementos del conjunto ("miembro1", "miembro2" y "miembro3"), se almacena en forma de SDS. Además de almacenar objetos, SDS también se usa para almacenar varios búferes.

Las cadenas C solo se usan en los casos en que la cadena no cambiará, como cuando se imprimen registros.

4. Tipo de objeto y codificación interna de Redis

Como se mencionó anteriormente, Redis admite 5 tipos de objetos, y cada estructura tiene al menos dos codificaciones; la ventaja de esto es que, por un lado, la interfaz está separada de la implementación, y cuando se necesita agregar o cambiar la codificación interna, el uso del usuario no se verá afectado, por otro lado, la codificación interna se puede cambiar de acuerdo con diferentes escenarios de aplicación para mejorar la eficiencia.

La codificación interna admitida por varios tipos de objetos de Redis se muestra en la siguiente figura (la versión de la figura es Redis 3.0, y la codificación interna se agrega en la versión posterior de Redis, que se omite; la codificación interna introducida en este capítulo se basa en 3.0):

Fuente de la imagen: "Diseño e implementación de Redis"

Con respecto a la conversión de la codificación interna de Redis, todo se ajusta a las siguientes reglas: la conversión de codificación se completa cuando Redis escribe datos, y el proceso de conversión es irreversible y solo se puede convertir de codificación de memoria pequeña a codificación de memoria grande.

1. Cuerda

(1. Información general

String es el tipo más básico, porque todas las claves son de tipo String, y los elementos de varios otros tipos complejos además de String también son String.

La longitud de la cadena no puede exceder los 512 MB.

(2) Codificación interna

Hay tres codificaciones internas del tipo cadena, y sus escenarios de aplicación son los siguientes:

  • int: entero largo de 8 bytes. Cuando el valor de la cadena es un número entero, este valor se representa mediante un número entero largo.
  • embstr: <= cadena de 39 bytes. Tanto embstr como raw usan redisObject y sds para guardar datos. La diferencia es que el uso de embstr solo asigna espacio de memoria una vez (por lo que redisObject y sds son continuos), mientras que raw necesita asignar espacio de memoria dos veces (asignar espacio para redisObject y sds respectivamente ). Por lo tanto, en comparación con raw, la ventaja de embstr es que asigna menos espacio al crear, libera menos espacio al eliminar y conecta todos los datos del objeto, lo que facilita su búsqueda. Las desventajas de embstr también son obvias: si la longitud de la cadena aumenta y es necesario reasignar la memoria, todo el objeto redisObject y sds deben reasignar espacio, por lo que embstr en redis se implementa como de solo lectura.
  • raw: cadenas de más de 39 bytes

Un ejemplo se muestra en la siguiente figura:

La longitud de la distinción entre embstr y raw es 39; porque la longitud de redisObject es de 16 bytes, y la longitud de sds es 9+longitud de cadena; por lo tanto, cuando la longitud de cadena es 39, la longitud de embstr es exactamente 16+9 +39 =64, jemalloc solo puede asignar una unidad de memoria de 64 bytes.

(3) Conversión de código

Cuando los datos int ya no son un número entero, o el tamaño excede el rango de long, se convierte automáticamente en raw.

Para embstr, debido a que su implementación es de solo lectura, al modificar el objeto embstr, primero se convertirá a raw y luego se modificará. Por lo tanto, siempre que se modifique el objeto embstr, el objeto modificado debe ser raw, sin importar si alcances tomó 39 bytes. Un ejemplo se muestra en la siguiente figura:

2. Lista

(1. Información general

Una lista (list) se usa para almacenar múltiples cadenas ordenadas, y cada cadena se denomina elemento; una lista puede almacenar 2^32-1 elementos. Las listas en Redis admiten la inserción y la ventana emergente en ambos extremos, y pueden obtener elementos en posiciones específicas (o rangos), que pueden actuar como matrices, colas, pilas, etc.

(2) Codificación interna

La codificación interna de la lista puede ser una lista comprimida (ziplist) o una lista enlazada de dos extremos (linkedlist).

Lista enlazada de dos extremos: Consiste en una estructura de lista y múltiples estructuras listNode; una estructura típica se muestra en la siguiente figura:

Fuente de la imagen: "Diseño e implementación de Redis"

Se puede ver en la figura que la lista enlazada de dos extremos guarda el puntero de la cabeza y el puntero de la cola al mismo tiempo, y cada nodo tiene punteros que apuntan hacia el frente y hacia atrás; la longitud de la lista se guarda en la lista enlazada, dup, free y match son nodos Los valores establecen funciones específicas del tipo, por lo que las listas enlazadas se pueden usar para contener valores de varios tipos diferentes. Cada nodo de la lista vinculada apunta a un redisObject cuyo tipo es una cadena.

Lista comprimida: la lista comprimida es desarrollada por Redis para ahorrar memoria. Es una estructura de datos secuenciales compuesta por una serie de bloques de memoria continuos especialmente codificados (en lugar de que cada nodo sea un puntero como una lista enlazada de dos extremos); la estructura específica es relativamente complicado. ,ligeramente. En comparación con la lista enlazada de dos extremos, la lista comprimida puede ahorrar espacio en la memoria, pero la complejidad es mayor cuando se modifican, agregan o eliminan operaciones; por lo tanto, cuando el número de nodos es pequeño, se puede usar la lista comprimida; pero cuando el número de nodos es grande, la lista enlazada de dos extremos todavía se utiliza rentable.

Las listas comprimidas no solo se utilizan para implementar listas, sino también hashes, listas ordenadas, muy utilizadas.

(3) Conversión de código

La lista comprimida se utilizará solo cuando se cumplan las dos condiciones siguientes al mismo tiempo: el número de elementos de la lista es inferior a 512; todos los objetos de cadena de la lista tienen menos de 64 bytes. Si una de las condiciones no se cumple, se utiliza una lista de dos extremos y la codificación solo se puede convertir de una lista comprimida a una lista enlazada de dos extremos, y la dirección inversa no es posible.

La siguiente figura muestra las características de la conversión de codificación de lista:

Entre ellos, una sola cadena no puede exceder los 64 bytes, para facilitar la asignación uniforme de la longitud de cada nodo; 64 bytes aquí se refiere a la longitud de la cadena, excluyendo la estructura SDS, porque la lista comprimida se almacena en continuo, bloques de memoria de longitud fija Cadena, sin necesidad de una estructura SDS para indicar la longitud. Cuando se trata de la lista comprimida más adelante, también se enfatizará que la longitud no exceda los bytes 64. El principio es similar a este.

3. hachís

(1. Información general

Hash (como estructura de datos) no es solo uno de los cinco tipos de objetos proporcionados por redis (en paralelo con cadenas, listas, conjuntos y combinaciones ordenadas), sino también la estructura de datos utilizada por Redis como base de datos de valores clave. Para facilitar la explicación, cuando se utiliza "hash interno" más adelante en este artículo, representa uno de los cinco tipos de objetos proporcionados por redis; el uso de "hash externo" se refiere a Redis como la base de datos de valores clave La estructura de datos utilizada .

(2) Codificación interna

La codificación interna utilizada por el hash interno puede ser una lista comprimida (ziplist) y una tabla hash (hashtable); el hash externo de Redis solo usa una tabla hash.

Las listas comprimidas se describieron anteriormente. En comparación con la tabla hash, la lista comprimida se usa en escenarios con una cantidad pequeña de elementos y longitudes de elementos pequeñas; su ventaja radica en el almacenamiento centralizado y el ahorro de espacio; al mismo tiempo, aunque la complejidad de operación para elementos también se cambia de O (1) a O(n), pero debido a la pequeña cantidad de elementos en el hash, no hay una desventaja obvia en el tiempo de la operación.

hashtable: una tabla hash consta de 1 estructura dict, 2 estructuras dicttht, 1 matriz de punteros dictEntry (llamada cubeta) y varias estructuras dictEntry.

En circunstancias normales (es decir, cuando la tabla hash no se repite), la relación entre cada parte se muestra en la siguiente figura:

La imagen está adaptada de: "Diseño e implementación de Redis"

A continuación se describe cada parte por separado de abajo hacia arriba:

dictEntrada

La estructura dictEntry se usa para almacenar pares clave-valor, y la estructura se define de la siguiente manera:

1

2

3

4

5

6

7

8

9

typedef struct dictEntry{

    void *key;

    union{

        void *val;

        uint64_tu64;

        int64_ts64;

    }v;

    struct dictEntry *next;

}dictEntry;

Entre ellos, las funciones de cada atributo son las siguientes:

  • clave: la clave en el par clave-valor;
  • val: el valor en el par clave-valor, que se implementa mediante una unión (es decir, una unión), y el contenido almacenado puede ser un puntero a un valor, un número entero de 64 bits o un número de 64 bits sin signo. entero;
  • next: apunta a la siguiente dictEntry, utilizada para resolver el problema de colisión hash

En un sistema de 64 bits, un objeto dictEntry ocupa 24 bytes (key/val/next cada uno ocupa 8 bytes).

balde

Bucket es una matriz y cada elemento de la matriz es un puntero a una estructura dictEntry. Las reglas de cálculo para el tamaño de la matriz de cubetas en redis son las siguientes: el 2^n más pequeño que sea mayor que dictEntry; por ejemplo, si hay 1000 dictEntry, el tamaño de la cubeta es 1024; si hay 1500 dictEntry, la cubeta el tamaño es 2048

dictado

La estructura del dictado es la siguiente:

1

2

3

4

5

6

typedef struct dictht{

    dictEntry **table;

    unsigned long size;

    unsigned long sizemask;

    unsigned long used;

}dictht;

Entre ellos, la descripción de la función de cada atributo es la siguiente:

  • El atributo de la tabla es un puntero al cubo;
  • El atributo de tamaño registra el tamaño de la tabla hash, es decir, el tamaño del cubo;
  • used registra el número de dictEntry utilizado;
  • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

dict

一般来说,通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现中,在dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。

dict结构如下:

1

2

3

4

5

6

typedef struct dict{

    dictType *type;

    void *privdata;

    dictht ht[2];

    int trehashidx;

} dict;

其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。

ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。

因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash。

(3)编码转换

如前所述,Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表。

只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节。如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。

下图展示了Redis内层的哈希编码转换的特点:

4、集合

(1)概况

集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。

一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。

(2)内部编码

集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。

哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。

整数集合的结构定义如下:

1

2

3

4

5

typedef struct intset{

    uint32_t encoding;

    uint32_t length;

    int8_t contents[];

} intset;

其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。

(3)编码转换

只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。

下图展示了集合编码转换的特点:

5、有序集合

(1)概况

有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

(2)内部编码

有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。

跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,略。

(3)编码转换

只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

下图展示了有序集合编码转换的特点:

五、应用举例

了解Redis的内存模型之后,下面通过几个例子说明其应用。

1、估算Redis内存使用量

要估算redis中的数据占据的内存大小,需要对redis的内存模型有比较全面的了解,包括前面介绍的hashtable、sds、redisobject、各种对象类型的编码方式等。

下面以最简单的字符串类型来进行说明。

假设有90000个键值对,每个key的长度是7个字节,每个value的长度也是7个字节(且key和value都不是整数);下面来估算这90000个键值对所占用的空间。在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr。

90000个键值对占据的内存空间主要可以分为两部分:一部分是90000个dictEntry占据的空间;一部分是键值对所需要的bucket空间。

每个dictEntry占据的空间包括:

1)       一个dictEntry,24字节,jemalloc会分配32字节的内存块

2)       一个key,7字节,所以SDS(key)需要7+9=16个字节,jemalloc会分配16字节的内存块

3)       一个redisObject,16字节,jemalloc会分配16字节的内存块

4)       一个value,7字节,所以SDS(value)需要7+9=16个字节,jemalloc会分配16字节的内存块

5)       综上,一个dictEntry需要32+16+16+16=80个字节。

bucket空间:bucket数组的大小为大于90000的最小的2^n,是131072;每个bucket元素为8字节(因为64位系统中指针大小为8字节)。

因此,可以估算出这90000个键值对占据的内存大小为:90000*80 + 131072*8 = 8248576。

下面写个程序在redis中验证一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public class RedisTest {

  public static Jedis jedis = new Jedis("localhost", 6379);

  public static void main(String[] args) throws Exception{

    Long m1 = Long.valueOf(getMemory());

    insertData();

    Long m2 = Long.valueOf(getMemory());

    System.out.println(m2 - m1);

  }

  public static void insertData(){

    for(int i = 10000; i < 100000; i++){

      jedis.set("aa" + i, "aa" + i); //key和value长度都是7字节,且不是整数

    }

  }

  public static String getMemory(){

    String memoryAllLine = jedis.info("memory");

    String usedMemoryLine = memoryAllLine.split("\r\n")[1];

    String memory = usedMemoryLine.substring(usedMemoryLine.indexOf(':') + 1);

    return memory;

  }

}

运行结果:8247552

理论值与结果值误差在万分之1.2,对于计算需要多少内存来说,这个精度已经足够了。之所以会存在误差,是因为在我们插入90000条数据之前redis已分配了一定的bucket空间,而这些bucket空间尚未使用。

作为对比将key和value的长度由7字节增加到8字节,则对应的SDS变为17个字节,jemalloc会分配32个字节,因此每个dictEntry占用的字节数也由80字节变为112字节。此时估算这90000个键值对占据内存大小为:90000*112 + 131072*8 = 11128576。

在redis中验证代码如下(只修改插入数据的代码):

1

2

3

4

5

public static void insertData(){

  for(int i = 10000; i < 100000; i++){

    jedis.set("aaa" + i, "aaa" + i); //key和value长度都是8字节,且不是整数

  }

}

运行结果:11128576;估算准确。

对于字符串类型之外的其他类型,对内存占用的估算方法是类似的,需要结合具体类型的编码方式来确定。

2、优化内存占用

了解redis的内存模型,对优化redis内存占用有很大帮助。下面介绍几种优化场景。

(1)利用jemalloc特性进行优化

上一小节所讲述的90000个键值便是一个例子。由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。

例如,如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。

(2)使用整型/长整型

如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。

(3)共享对象

利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享。

考虑这样一种场景:论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间。

(4)避免过度设计

然而需要注意的是,不论是哪种优化场景,都要考虑内存空间与设计复杂度的权衡;而设计复杂度会影响到代码的复杂度、可维护性。

如果数据量较小,那么为了节省内存而使得代码的开发、维护变得更加困难并不划算;还是以前面讲到的90000个键值对为例,实际上节省的内存空间只有几MB。但是如果数据量有几千万甚至上亿,考虑内存的优化就比较必要了。

3、关注内存碎片率

内存碎片率是一个重要的参数,对redis 内存的优化有重要意义。

如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。

如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(即swap);由于虚拟内存的存取速度比物理内存差很多(2-3个数量级),此时redis的访问速度可能会变得很慢。因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据。

要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。

六、参考文献

《Redis开发与运维》

《Redis设计与实现》

https://redis.io/documentation

http://redisdoc.com/server/info.html

https://www.cnblogs.com/lhcpig/p/4769397.html

https://searchdatabase.techtarget.com.cn/7-20218/

http://www.cnblogs.com/mushroom/p/4738170.html

http://www.imooc.com/article/3645

http://blog.csdn.net/zhengpeitao/article/details/76573053

Supongo que te gusta

Origin blog.csdn.net/qq_41872328/article/details/129918459
Recomendado
Clasificación