Mejores prácticas de Redis, parte 2

La segunda parte del ciclo de traducción Redis Best Practices de Redis Labs, y analiza los patrones de interacción y los patrones de almacenamiento de datos.

La primera parte está aquí .

Patrones de interacción


Redis puede funcionar no solo como un DBMS tradicional, sino que también sus estructuras y comandos se pueden usar para intercambiar mensajes entre microservicios o procesos. El uso generalizado de los clientes de Redis, la velocidad y la eficiencia del servidor y el protocolo, así como las estructuras clásicas integradas le permiten crear sus propios flujos de trabajo y mecanismos de eventos. En este capítulo, cubriremos los siguientes temas:

  • cola de eventos;
  • bloqueo con Redlock;
  • Pub / Sub;
  • Eventos distribuidos.

Cola de eventos


Las listas en Redis son listas de líneas ordenadas, muy similares a las listas vinculadas con las que puede estar familiarizado. Agregar un valor a una lista (push) y eliminar un valor de una lista (pop) son operaciones muy livianas. Como puede imaginar, esta es una muy buena estructura para administrar una cola: agregue elementos al principio y léalos desde el final (FIFO). Redis también proporciona características adicionales que hacen que este patrón sea más eficiente, confiable y fácil de usar.

Las listas tienen un subconjunto de comandos que le permiten ejecutar el comportamiento de "bloqueo". El término "bloqueo" se refiere a una conexión con un solo cliente. De hecho, estos comandos no permiten que el cliente haga nada hasta que aparezca un valor en la lista o hasta que expire el tiempo de espera. Esto elimina la necesidad de sondear Redis, esperando el resultado. Como el cliente no puede hacer nada mientras espera un valor, necesitaremos dos clientes abiertos para ilustrar esto:
# #Cliente 1Cliente 2
1
> BRPOP my-q 0
[espere valor]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[cliente desbloqueado, listo para aceptar comandos]
3
> BRPOP my-q 0
[espere valor]

En este ejemplo, en el paso 1, vemos que el cliente bloqueado no devuelve nada inmediatamente, ya que no contiene nada. El argumento final es el tiempo de espera. Aquí 0 significa expectativa eterna. En la segunda línea , se ingresa un valor en my-q , y el primer cliente sale inmediatamente del estado de bloqueo. En la tercera línea, se vuelve a llamar a BRPOP (puede hacer esto en un bucle en la aplicación), y el cliente también espera el siguiente valor. Al presionar "Ctrl + C" puede romper el bloqueo y salir del cliente.

Vamos a revertir el ejemplo y ver cómo funciona BRPOP con una lista no vacía:
# #Cliente 1Cliente 2
1
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4 4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5 5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6 6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7 7
> BRPOP my-q 0
[espere valor]

En los pasos 1-3, agregamos 3 valores a la lista y vemos que la respuesta crece, indicando el número de elementos en la lista. El paso 4, a pesar de llamar a BRPOP, devuelve el valor de inmediato. Esto se debe a que el comportamiento de bloqueo ocurre solo cuando no hay valores en la cola. Podemos ver la misma respuesta instantánea en los pasos 5-6 porque esto se hace para cada elemento de la cola. En el paso 7, BRPOP no encuentra nada en la cola y bloquea al cliente hasta que se agrega algo.

A menudo, las colas representan un trabajo que debe hacerse en otro proceso (trabajador). En este tipo de carga de trabajo, es importante que el trabajo no desaparezca si el trabajador cae por algún motivo durante la ejecución. Redis admite este tipo de cola. Para hacer esto, use el comando BRPOPLPUSH en lugar de BRPOP. Ella espera un valor en una lista, y tan pronto como aparece allí, lo coloca en otra lista. Esto se hace atómicamente, por lo que es imposible que dos trabajadores cambien el mismo valor. Vamos a ver cómo funciona:
# #Cliente 1Cliente 2
1
> LINDEX worker-q 0
(nil)
2[Si el resultado no es nulo, de alguna manera trátelo y vaya al paso 4]
3
> LREM worker-q -1 [   1]
(integer) 1
[volver al paso 1]
4 4
> BRPOPLPUSH my-q worker-q 0
[espere valor]
5 5
> LPUSH my-q hello
"hello"
[cliente desbloqueado, listo para aceptar comandos]
6 6[manejar hola]
7 7
> LREM worker-q -1 hello
(integer) 1
8[volver al paso 1]

En los pasos 1-2, no hacemos nada, porque trabajador-q está vacío. Si algo ha regresado, lo procesamos y lo eliminamos, y nuevamente volvemos al paso 1 para verificar si algo ha entrado en la cola. Por lo tanto, primero limpiamos la cola del trabajador y realizamos el trabajo existente. En el paso 4, esperamos hasta que el valor aparezca en my-q , y cuando lo hace, se transfiere atómicamente a trabajador-q . Luego, de alguna manera procesamos "hola" , después de eso lo eliminamos de trabajador-q y volvemos al paso 1. Si el proceso muere en el paso 6, el valor aún permanece en trabajador-q . Después de reiniciar el proceso, eliminaremos inmediatamente todo lo que no se eliminó en el paso 7.

Este patrón reduce en gran medida la probabilidad de pérdida de empleo, pero solo si el trabajador muere entre los pasos 2 y 3 o 5 y 6, lo cual es poco probable, pero las mejores prácticas lo tendrán en cuenta en la lógica del trabajador.

Cerradura con redlock


Algunas veces en el sistema es necesario bloquear algunos recursos. Esto puede ser necesario para aplicar cambios importantes que no pueden resolverse en un entorno competitivo. Objetivos de bloqueo:

  • permitir que un solo trabajador capture el recurso;
  • ser capaz de liberar de manera confiable el objeto de bloqueo;
  • No bloquee el recurso firmemente (debe desbloquearse después de un cierto período de tiempo).

Redis es una buena opción para implementar el bloqueo, ya que tiene un modelo de datos simple basado en claves, y cada fragmento es de un solo subproceso y bastante rápido. Hay una excelente implementación de bloqueo usando Redis llamada Redlock.
Los clientes de Redlock están disponibles para casi todos los idiomas, sin embargo, es importante saber cómo funciona Redlock para usarlo de manera segura y efectiva.

Primero, debe comprender que Redlock está diseñado para ejecutarse en al menos 3 máquinas con instancias de Redis independientes. Esto elimina el único punto de falla en su mecanismo de bloqueo, lo que puede llevar a un punto muerto de todos los recursos. Otro punto a entender es que aunque los relojes en las máquinas no deben estar 100% sincronizados, deben funcionar de la misma manera: el tiempo se mueve a la misma velocidad: un segundo en la máquina y lo mismo que un segundo en la máquina B.

Establecer un objeto de bloqueo con Redlock comienza obteniendo una marca de tiempo con una precisión de milisegundos. También debe indicar de antemano el tiempo de bloqueo. Luego, el objeto de bloqueo se establece configurando (SET) la clave con un valor aleatorio (solo si esta clave aún no existe) y configurando el tiempo de espera para la clave. Esto se repite para cada instancia independiente. Si la instancia cae, se omite inmediatamente. Si el objeto de bloqueo se instaló correctamente en la mayoría de los casos antes de que expire el tiempo de espera, se considera capturado. El tiempo para instalar o actualizar el objeto de bloqueo es la cantidad de tiempo que lleva alcanzar el estado de bloqueo, menos el tiempo de bloqueo predefinido. En caso de error o tiempo de espera, desbloquee todas las instancias e intente nuevamente.

Para liberar un objeto de bloqueo, es mejor usar un script Lua que verificará si el valor aleatorio esperado está en el conjunto de claves. Si lo hay, puede eliminarlo, de lo contrario es mejor dejar las llaves, ya que estos pueden ser objetos de bloqueo más nuevos.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

El proceso Redlock proporciona buenas garantías y la ausencia de un solo punto de falla, por lo que puede estar completamente seguro de que se distribuirán objetos de bloqueo único y que no se producirán bloqueos mutuos.

Pub / Sub


Además del almacenamiento de datos, Redis también se puede utilizar como plataforma Pub / Sub (editor / suscriptor). En este patrón, un editor puede emitir mensajes a cualquier número de suscriptores del canal. Estos son mensajes basados ​​en el principio de "disparar y olvidar", es decir, si el mensaje se libera y el suscriptor no existe, el mensaje desaparece sin posibilidad de recuperación.
Al suscribirse al canal, el cliente ingresa al modo de suscriptor y ya no puede llamar a los comandos; se vuelve de solo lectura. El editor no tiene tales restricciones.

Puedes suscribirte a más de un canal. Comenzamos suscribiéndonos a los dos canales meteorológicos y deportivos utilizando el comando SUBSCRIBE:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

En un cliente separado (otra ventana de terminal, por ejemplo) podemos publicar mensajes en cualquiera de estos canales usando el comando PUBLICAR:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

El primer argumento es el nombre del canal, el segundo es el mensaje. El mensaje puede ser cualquiera, en este caso es una cuenta codificada en el juego. El comando devuelve el número de clientes a los que se entregará el mensaje. En el cliente suscriptor, vemos inmediatamente el mensaje:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

La respuesta contiene tres elementos: una indicación de que se trata de un mensaje, un canal de suscripción y, de hecho, un mensaje. El cliente inmediatamente después de recibir vuelve a escuchar el canal.

Volviendo al editor, podemos publicar otro mensaje:

> PUBLISH weather snow/-4c
(integer) 1

En el suscriptor veremos el mismo formato, pero con un canal diferente con el mensaje:

1) "message"
2) "weather"
3) "snow/-4c"

Publiquemos un mensaje en un canal donde no hay suscriptores:

> PUBLISH currency CADUSD/0.787
(integer) 0

Como nadie está escuchando el canal de moneda , la respuesta será 0. Este mensaje se ha ido, y los clientes que después de suscribirse a este canal no recibirán una notificación sobre este mensaje: fue enviado y olvidado.

Además de suscribirse a un solo canal, Redis permite suscribirse a canales por máscara. La máscara de estilo glob se pasa al comando PSUBSCRIBE:

> PSUBSCRIBE sports:*

El cliente recibirá los mensajes de todos los canales, empezando por los deportes: . En otro cliente, llame a los siguientes comandos:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

Tenga en cuenta que los dos primeros equipos devuelven 1, mientras que el último devuelve 0. Y aunque no estamos suscritos directamente a deportes: hockey o deportes: baloncesto , el cliente recibe mensajes a través de una suscripción por máscara. En la ventana cliente-suscriptor, podemos ver que solo hay resultados para los canales que coinciden con la máscara.

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

Esta salida es ligeramente diferente de la salida del comando SUBSCRIBE porque contiene la máscara en sí, así como el nombre real del canal.

Eventos distribuidos


El esquema de mensajes de Pub / Sub de Redis se puede ampliar para crear eventos distribuidos interesantes. Digamos que tenemos una estructura que se almacena en una tabla hash, pero queremos actualizar clientes solo cuando un solo campo excede el valor numérico establecido por el suscriptor. Escucharemos los canales por máscara y extraeremos el estado hash . En este ejemplo, estamos interesados ​​en update_status con valores 5-9.

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

Para cambiar el valor de status / error_level , necesitamos dos comandos que se puedan ejecutar secuencialmente o en el bloque MULTI / EXEC. El primer comando establece el nivel, y el segundo publica una notificación con el valor codificado en el propio canal.

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

En la primera ventana, vemos que el mensaje ha sido recibido, y luego puede cambiar a otro cliente y llamar al comando HGETALL:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

También podemos usar este método para actualizar la variable local de algún proceso largo. Esto puede permitir que varias instancias del mismo proceso intercambien datos en tiempo real.

¿Por qué es este patrón mejor que usar Pub / Sub? Cuando el proceso se reinicia, puede obtener todo el estado y comenzar a escuchar. Los cambios se sincronizarán entre cualquier número de procesos.

Patrones de almacenamiento de datos


Existen varios patrones para almacenar datos estructurados en Redis. En este capítulo consideraremos lo siguiente:

  • almacenamiento de datos en JSON;
  • instalaciones de almacenamiento.

Almacenamiento de datos JSON


Hay varias opciones para almacenar datos JSON en Redis. La forma más común es serializar el objeto de antemano y guardarlo en una clave especial:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

Parece simple, pero tiene algunos inconvenientes muy serios:

  • la serialización requiere recursos informáticos del cliente para leer y escribir;
  • El formato JSON aumenta el tamaño de los datos;
  • Redis solo tiene una forma indirecta de manejar los datos en JSON.

Los primeros puntos pueden ser insignificantes en pequeñas cantidades de datos, pero los costos aumentarán a medida que los datos crezcan. Sin embargo, el tercer punto es el más crítico.

Antes de Redis 4.0, la única forma de trabajar con JSON dentro de Redis era usar un script Lua en el módulo cjson. Esto resolvió parcialmente el problema, aunque todavía seguía siendo un cuello de botella y creaba problemas adicionales con el aprendizaje de Lua. Además, muchas aplicaciones simplemente recibieron la cadena JSON completa, la deserializaron, trabajaron con los datos, los serializaron de nuevo y los volvieron a guardar. Este es un antipatrón. Existe un gran riesgo de perder datos de esta manera.

# #Instancia de aplicación n. ° 1Instancia de aplicación # 2
1
> GET my-car
2[deserializar, cambiar el color de la máquina y volver a serializar]
> GET my-car
3
> SET my-car

[nuevo valor de la instancia # 1]
[deserializar, cambiar el modelo de máquina y serializar nuevamente]
4 4
> SET my-car

[nuevo valor de la instancia # 2]
5 5
> GET my-car

El resultado en la línea 5 mostrará cambios solo en la instancia 2, y el cambio de color por la instancia 1 se perderá.

Redis versión 4.0 y superior tiene la capacidad de usar módulos. ReJSON es un módulo que proporciona un tipo de datos y comandos especiales para la interacción directa con él. ReJSON guarda los datos en formato binario, lo que reduce el tamaño de los datos almacenados, proporciona un acceso más rápido a los elementos sin perder tiempo en la des / serialización.

Para usar ReJSON, debe instalarlo en un servidor Redis o habilitarlo en Redis Enterprise.

El ejemplo anterior usando ReJSON se vería así:

# #Instancia de aplicación n. ° 1Instancia de aplicación # 2
1
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}‘
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

ReJSON proporciona una forma más segura, rápida e intuitiva de trabajar con datos JSON en Redis, especialmente en los casos en que son necesarios cambios atómicos en elementos anidados.

Almacenamiento de objetos


A primera vista, el tipo de datos estándar de la "tabla hash" de Redis puede parecer muy similar a un objeto JSON u otro tipo. Es mucho más fácil hacer que los campos sean una cadena o un número y evitar estructuras anidadas. Sin embargo, después de calcular la "ruta" a cada campo, puede "aplanar" el objeto y guardarlo en la tabla hash de Redis.

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

Usando JSONPath (XPath para JSON), podemos representar cada elemento en el mismo nivel de la tabla hash:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

Para mayor claridad, los comandos se enumeran por separado, pero se pueden pasar muchos parámetros a HSET.

Ahora puede solicitar el objeto completo o su campo individual:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

Aunque esto proporciona una forma rápida y útil de recuperar un objeto almacenado en Redis, tiene sus inconvenientes:

  • En diferentes lenguajes y bibliotecas, la implementación de JSONPath puede ser diferente, causando incompatibilidad. En este caso, vale la pena serializar y deserializar los datos con una herramienta;
  • soporte de matriz:
    • matrices dispersas pueden ser problemáticas;
    • Es imposible realizar muchas operaciones, como insertar un elemento en el medio de una matriz.

  • Consumo innecesario de recursos en claves JSONPath.

Este patrón es más o menos lo mismo que ReJSON. Si ReJSON está disponible, en la mayoría de los casos es mejor usarlo. Sin embargo, almacenar objetos en la forma anterior tiene una ventaja sobre ReJSON: la integración con el equipo Redis SORT. Sin embargo, este comando es computacionalmente complejo y es un tema complejo separado más allá del alcance de este patrón.

La siguiente parte concluyente cubrirá patrones de series de tiempo, patrones de límite de velocidad, patrones de filtro Bloom, contadores y el uso de Lua en Redis.

PD: Traté de adaptar el texto de estos artículos en inglés "bárbaro" lo más posible al ruso, pero si crees que en algún lugar la idea es incomprensible o incorrecta, corrígeme en los comentarios.

Source: https://habr.com/ru/post/undefined/


All Articles