Skip to content

redis

dartis (y 2)

En este artículo se continúa la revisión de las características ofrecidas por dartis, un cliente para Redis escrito en Dart que publiqué hace unos días.

Pub/Sub

Redis permite que los clientes entren en un modo de funcionamiento especial en el que se implementa el patrón Publish/Subscribe. En este modo los clientes pueden suscribirse a canales y recibir mensajes a través de ellos. Es decir, que Redis se comporta como un sistema de mensajería. En este modo los clientes sólo pueden ejecutar los comandos SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PING y QUIT.

El servidor envía, tanto las respuestas a las suscripciones a canales, como los propios mensajes publicados en dichos canales, en un stream de eventos con una estructura de mensajes distinta a la que se recibe cuando el cliente se encuentra operando en el modo normal, por lo que se debe conocer en todo momento en que modo se encuentra trabajando el cliente para poder procesar correctamente las respuestas recibidas del servidor.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

Tener una clase especializada para implementar este modo permite reducir la complejidad del código. Máxime cuando Redis sólo permite a los clientes en este modo ejecutar un conjunto de comandos muy concretos y salir terminando la conexión con el servidor. Aunque impide que el cliente ejecute otros comandos antes de entrar en este modo, como por ejemplo validarse contra el servidor utilizando una clave.

Monitor

Redis permite también que los clientes trabajen en modo monitor. En este modo no pueden ejecutar ningún comando, se limitan a recibir mensajes del servidor. Los mensajes recibidos contienen un texto con detalles acerca de todos los comandos ejecutados por todos los clientes contra el servidor. Es decir, que cada vez que un cliente ejecuta un comando, el servidor envía un mensaje informando de tal acción a todos los clientes que se encuentran en modo monitor. Esto resulta de utilidad para depurar aplicaciones de forma no intrusiva.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

De igual forma que en el modo del apartado anterior, disponer de una clase especializada reduce la complejidad de la implementación, pero no permite ejecutar ningún comando previo a la entrada en este modo.

Inline Commands

Redis permite la ejecución de comandos sin tener que implementar el protocolo RESP. En este modo los comandos se envían como cadenas de texto plano al servidor. Es similar a como se haría a través de una sesión Telnet, escribiendo directamente en una consola de texto.

Las respuestas recibidas desde el servidor en este modo están formateadas siguiendo el protocolo RESP, por lo que este modo es adecuado para comprobar la respuesta exacta del servidor ante un determinado comando, evitando cualquier tipo de agente intermedio que la procese o altere de alguna forma.

Transacciones

Redis permite ejecutar varios comandos dentro del contexto de una transacción. Aunque no es una acción de “todo o nada” como habitualmente se sobreentiende que funciona una transacción con la mayoría de software existente.

Una transacción empieza en Redis ejecutando el comando MULTI y termina ejecutando el comando EXEC. Todos los comandos que se ejecutan después de MULTI se encolan y se ejecutan cuando se llama a EXEC. Si se produce un error encolando un comando la transacción se aborta en el momento que se llama a EXEC, no en el momento que se produce el error encolando el comando. Y si se produce un error durante la ejecución de un comando encolado se continúa con el siguiente comando encolado, no se produce un rollback de lo ejecutado hasta el momento.

EXEC retorna el resultado de la ejecución de todos los comandos ejecutados en el contexto de la transacción, tanto los terminados con éxito como los terminados con error, para dar la oportunidad al cliente de actuar en función del resultado individual de cada comando.

Con este patrón no se puede utilizar await  sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene la respuesta del servidor, algo que no ocurre hasta ejecutar EXEC.

Una transacción iniciada con MULTI puede abortarse ejecutando el command DISCARD antes de llamar a EXEC. El comando DISCARD termina la transacción en curso descartando todos los comandos encolados hasta el momento.

Por último, comentar que en el contexto de una transacción no se debe utilizar el comando CLIENT REPLY, ya que en determinados casos el cliente puede perder la sincronía con el servidor. Es por ello que las transacciones en Redis están documentadas como una funcionalidad a ser deprecada, prefiriéndose el uso de scripts en Lua en el servidor.

Scripts Lua

Redis permite ejecutar scripts en Lua en el servidor. Esta característica es ofrecida a través de comandos, por lo que no requiere ninguna implementación especial por parte del cliente.

La única característica particular a tener en cuenta con esta funcionalidad es que la salida de la ejecución de un script puede ser de cualquier tipo. Lo mismo puede resultar en una primitiva, como una cadena de texto o un entero, o un array de valores heterogéneos. Para lidiar con esta situación, dartis admite un parámetro que permite mapear la respuesta del servidor en cualquier tipo que se quiera.

En el ejemplo del código anterior, el mapper convierte la respuesta del servidor en una lista de cadenas de texto utilizando las facilidades que ofrece dartis para crear serializadores y deserializadores personalizados.

Serializadores/Deserializadores

dartis tiene un sistema de conversores dinámico que permite añadir conversores nuevos o sobreescribir los existentes. Un conversor es una clase que convierte una primitiva en una lista de bytes de cara a ser enviado al servidor, y convierte una lista de respuestas del servidor o bytes en una primitiva de cara a que un cliente pueda utilizarla en una aplicación.

Por defecto hay registrados conversores para los tipos de uso más habitual como enteros, decimales, cadenas de texto y listas. Utilizando UTF-8 por defecto para las cadenas de texto.

Los conversores que transforman primitivas en listas de bytes se denominan codificadores, y son clases que se crean extendiendo de Converter o Encoder. Por ejemplo, el siguiente código muestra un conversor que transforma instancias del tipo DateTime de Dart en listas de bytes.

Por su parte, los conversores que transforman listas de respuestas del servidor o bytes en una primitiva se denominan decodificadores, y son clases que se crean extendiendo de Converter o Decoder. Por ejemplo, el siguiente código muestra un conversor que transforma una respuesta del servidor en una instancia del tipo DateTime de Dart.

Todos los codificadores se registran utilizando el atributo codec del cliente.

Permitir definir tipos personalizados facilita trabajar de forma sencilla con los comandos propios que pueda definir cualquier módulo para Redis. Siendo un módulo una extensión que se puede añadir a un servidor Redis, a modo de plugin, y que dartis soporta de manera natural.

Y estas son básicamente las principales características de la librería. En el propio repositorio del proyecto puede encontrarse más documentación y ejemplos de uso.

Para terminar, comentar que el proyecto está desarrollado utilizando Visual Studio Code como IDE con tan sólo un par de extensiones. EditorConfig para garantizar la uniformidad del formato de los ficheros, y Dart Code para la integración de las herramientas del SDK, en particular el formateador de código y el analizador de código estático. Travis como servidor de integración continua y Coveralls para el análisis de la cobertura del código. El proyecto tiene implementadas unos trescientos casos de prueba, incluyendo tanto pruebas unitarias como de integración, con un noventa por ciento de cobertura.

dartis (1)

Hace un par de días liberé dartis, un cliente para Redis escrito en Dart. Empecé a desarrollarlo para probar algunas de las novedades de la versión 2 de Dart y al final he acabado con un cliente sencillo pero bastante completo.

RESP

REdis Serialization Protocol (RESP) es el protocolo que implementa Redis para comunicar los clientes con el servidor. Es un protocolo muy básico sobre TCP, con tan sólo cinco tipos de paquetes distintos. Los mensajes son todos de tipo texto, con un primer carácter identificador del tipo de paquete, seguido de una longitud en algunos casos, a continuación el contenido del mensaje en sí mismo, y un par de caracteres de terminación.

El hecho de que sea un protocolo de tipo texto tiene la ventaja de que resulta fácil de implementar y depurar, pero tiene el inconveniente de que hay que convertir entre binario y texto continuamente, en particular las longitudes de los mensajes. En principio he optado por utilizar las facilidades del propio lenguaje para las conversiones, sin realizar ninguna medición del rendimiento, pero podría estudiarse la posibilidad de hacer una implementación más específica.

Por otra parte, debido a que los paquetes no contienen ningún tipo de identificador, es necesario guardar la secuencia de cada comando enviado de forma ordenada y casarla contra las respuestas recibidas en el mismo orden. Con el hándicap de que es posible deshabilitar las respuestas del servidor, por lo que en algunos casos los paquetes han de enviarse sin esperar respuesta. En determinadas circunstancias muy concretas el protocolo no impide que se produzcan pérdidas de sincronía entre cliente y servidor.

Conexión

La conexión al servidor se realiza siguiendo el típico patrón consistente en utilizar un método estático en la clase del cliente a modo de factoría.

Como parámetro se admite una cadena de conexión siguiendo el formato clásico de protocolo, host y puerto. Lo que posibilita una configuración muy sencilla y ampliable.
Redis permite trabajar con varias bases de datos dentro de una misma instancia, pero esta forma de trabajo no está totalmente soportada cuando se utiliza una instalación de Redis en cluster, por lo que es mejor no utilizar esta característica si no es absolutamente necesaria. Resulta más sencillo levantar más instancias.

Redis permite proteger de forma opcional con una clave los accesos al servidor, pero normalmente las instancias de Redis no se encuentran abiertas al mundo exterior, sino como dentro de una DMZ, por lo que no suele utilizarse, aunque ello depende en gran medida de las políticas de seguridad de cada cual.

Algunos puntos abiertos en la implementación de las conexiones con el servidor son la posibilidad de indicar un timeout, realizar reconexiones automáticas en caso de pérdida de conexión, y un pool de conexiones.

Comandos

Un cliente le comunica a un servidor Redis lo que quiere hacer a través de comandos. Y existen más de 200 comandos distintos. dartis implementa una interface genérica que permite ejecutar cualquier tipo de comando, a la vez que ofrece una interface más específica que expone todos los comandos ofrecidos por Redis de forma fuertemente tipada.

dartis funciona de forma totalmente asíncrona, por lo que todos los comandos retornan un Future que se completa cuando el servidor retorna el resultado.

Además, el conjunto de comandos está expuesto utilizando Generics, lo que permite que cada aplicación que utilice la librería pueda decidir el tipo concreto de datos que quiere utilizar, en vez de limitarse a utilizar String como hacen otras librerías.

Redis almacena secuencias de bytes, no cadenas de textos, por lo que puede utilizarse para gestionar cualquier tipo de información.

Los comandos están expuestos en forma de vista sobre el cliente, es decir, es la misma instancia pero exponiendo sólo una interface sobre la misma, por lo que pueden obtenerse distintas vistas del mismo cliente para trabajar con distintos tipos de datos.

Es seguro trabajar con varias vistas a un mismo tiempo, ya que todas utilizan el mismo cliente.

Pipelining

Redis permite enviar una al servidor una serie de comandos a un mismo tiempo, en vez de uno a uno. Aunque en la práctica esta es más una característica propia de las comunicaciones que de Redis. El cliente puede enviar más de un comando dentro de un mismo paquete TCP. Y el servidor puede procesar paquetes TCP que contengan más de un comando.
Por defecto dartis sólo envía un comando en cada paquete TCP, deshabilitando de forma explícita el algoritmo de Nagle sobre el socket, para garantizar que cada comando se envía de forma inmediata al servidor, pero permite activar el pipelining llamando al método pipeline:

En buena lógica, con este patrón no se puede utilizar await sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene respuesta del servidor, y los comandos no se envían al servidor hasta que se vacía el pipeline con la llamada al método flush.

Otra opción es utilizar la lista de Futures retornada por el método flush para esperar hasta que se completen todos los comandos.

Esta forma de trabajo se utiliza sobre todo para realizar cargas masivas de información o clientes con casos de uso concretos que requieren un mayor rendimiento.

Fire And Forget

Redis permite que los clientes indiquen al servidor que no quieren recibir respuestas del servidor. Es decir, se pueden enviar comandos para su ejecución y desentenderse del resultado (fire and forget).

Esta es una característica que Redis ofrece a través del comando CLIENT REPLY. El comando necesita un parámetro que admite el valor OFF para deshabilitar todas las respuestas, SKIP para deshabilitar sólo la respuesta del comando siguiente, y ON para habilitar todas las respuestas.

En este modo los comandos se completan de forma inmediata con el valor null cuando las respuestas del servidor están deshabilitadas.

La implementación de esta característica no es compleja, pero requiere que el cliente detecte de manera explícita que se está ejecutando el comando CLIENT REPLY, y llevar la cuenta de para que comandos se debe esperar respuesta y para cuáles no. Si el propio comando CLIENT REPLY es el que falla entonces puede llegar a perderse la sincronía entre servidor y cliente, aunque se presupone una casuística con una probabilidad muy baja de que suceda.

Redis (y 2)

Continuando con el repaso de las características básicas de Redis, en este artículo se revisan los tipos que representan estructuras agregadas de elementos.

Hash

Este tipo almacena pares claves-valor asociados a un nombre. Se comporta como los arrays asociativos presentes en la mayoría de lenguajes de programación.

El caso de uso más habitual que implementan es almacenar información relacionada con una entidad, a modo de atributos de un objeto.

Los comandos disponibles para trabajar con hashes siguen la notación característica de Redis para estructuras, donde cada comando empieza con una letra que hace referencia al tipo con el que operan. Por ejemplo, los comandos que empiezan por H operan con hashes.

Comandos específicos para el tipo hash:

  • HSET, establece el valor de un campo
  • HSETNX, establece el valor de un campo, pero sólo si no existía
  • HMSET, establece el valor de una serie de campos
  • HGET, obtiene el valor de un campo
  • HGETALL, obtiene todos los campos y valores
  • HMGET, obtiene los valores de los campos dados
  • HDEL, borra un campo
  • HINCRBY, incrementa el valor entero de un campo por un valor dado
  • HINCRBYFLOAT, incrementa el valor decimal de un campo por un valor dado
  • HSTRLEN, obtiene la longitud de un valor almacenado en un campo
  • HEXISTS, comprueba si un campo existe
  • HLEN, obtiene el número de campos
  • HKEYS, obtiene todos los nombres de campos
  • HVALS, obtiene todos los valores
  • HSCAN, itera por los campos

Los hashes de Redis de tamaño reducido realizan un uso muy eficiente de la memoria y se recomienda su uso cuando el número de claves, y sus valores, sean de reducido tamaño.

List

Las listas en Redis están implementadas como listas enlazadas, lo que quiere decir que la inserción y borrado son operaciones rápidas que se ejecutan en un tiempo constante, frente el acceso a los elementos por su índice que es una operación más costosa.

El caso de uso más habitual es almacenar las últimas N ocurrencias de un determinado evento o tipo de objeto. Por ejemplo, se pueden utilizar listas para almacenar los IDs de los últimos artículos consultados en la web de una tienda, de forma que puedan recuperarse muy rápidamente sin necesidad de realizar una consulta costosa sobre una base de datos relacional.

Comandos específicos para el tipo list:

  • LPUSH, inserta elementos por la cabecera
  • LPUSHX, inserta un elemento en la cabecera, pero sólo si la lista existe
  • RPUSH, inserta elementos por la cola
  • RPUSHX, inserta un elemento por la cola, pero sólo si la lista existe
  • LPOP, obtiene y borra el elemento cabecera
  • RPOP, obtiene y borra el elemento de la cola
  • RPOPLPUSH, mueve el elemento de la cola a otra lista
  • LINSERT, inserta un elemento en un índice dado
  • LSET, establece el valor de un elemento en un índice dado
  • LINDEX, obtiene el elemento de un índice dado
  • LRANGE, obtiene los elementos de un rango dado
  • LREM, elimina elementos
  • LTRIM, elimina elementos en un rango dado
  • LLEN, obtiene el número de elementos
  • BLPOP, obtiene y borra el elemento cabecera de forma bloqueante
  • BRPOP, obtiene y borra el elemento de la cola de forma bloqueante
  • BRPOPLPUSH, mueve el elemento de la cola a otra lista de forma bloqueante

Los comandos permiten tratar las listas como pilas y colas añadiendo y eliminando elementos por los dos extremos, operaciones ambas muy rápidas al estar implementadas como listas enlazadas. Los nombres de los comandos utilizan la letra L (left) para indicar que la operación afecta a la cabecera de la lista y R (right) para indicar que afecta a la cola.

Notar además que los comandos que acceden a un rango de elementos utilizan el cero como índice para hacer referencia al primer elemento, y un índice negativo para hacer referenciar a los elementos por el final.

Un caso de uso especial de las listas es su utilización como colas siguiendo un modelo de suscriptores/consumidores. Los comando BLPOP y BRPOP extraen un elemento de una lista, bloqueando al cliente hasta que haya un elemento disponible, si la lista está vacía, o hasta que se sobrepase un tiempo de espera indicado como parámetro de la operación. Usando 0 como tiempo de espera el cliente espera indefinidamente.

Para probar el funcionamiento de Redis como cola de mensajes bloqueante basta con abrir un segundo cliente desde línea de comandos y ejecutar el comando BLPOP o BRPOP sobre una lista creada desde el primer cliente. Si la lista tiene elementos el segundo cliente retornará inmediatamente, pero si no lo tiene se bloqueará hasta que el primer cliente añada un nuevo elemento o se supere el tiempo de espera. Es realmente sencillo e inmediato. Para casos de usos más sofisticados se puede utilizar el comando BRPOPLPUSH, que extrae un elemento de una lista y lo inserta en otra, lo que puede resultar de utilidad para evitar la pérdida de mensajes, almacenándolos en una lista de trabajo temporal.

Set

Los conjuntos en Redis son colecciones desordenadas de elementos. Un mismo elemento sólo puede añadirse una única vez a un mismo conjunto.

El caso de uso habitual para los conjuntos es su utilización como almacén de referencias de unas entidades relacionadas con otras. Como las foreign keys en una base de datos relacional. Por ejemplo, para almacenar los ids de las etiquetas (tags) de las entradas (posts) de un blog.

Comandos disponibles con el tipo list:

  • SADD, añade un elemento
  • SMEMBERS, obtiene todos los elementos
  • SISMEMBER, comprueba si un elemento pertenece a un conjunto
  • SRANDMEMBER, obtiene elementos de forma aleatoria
  • SPOP, obtiene y borra elementos de forma aleatoria
  • SMOVE, mueve un elemento a otro conjunto
  • SREM, elimina elementos
  • SUNION, obtiene la unión de una serie de conjuntos
  • SUNIONSTORE, obtiene la unión de una serie de conjuntos y la almacena en otro
  • SINTER, obtiene la intersección de una serie de conjuntos
  • SINTERSTORE, obtiene la intersección de una serie de conjuntos y la almacena en otro
  • SDIFF, obtiene la diferencia de un conjunto contra una serie de conjuntos
  • SDIFFSTORE, obtiene la diferencia de un conjunto contra una serie de conjuntos y la almacena en otro
  • SCARD, obtiene el número de elementos
  • SSCAN, itera por los elementos

Los casos de uso de los conjuntos a veces parecen un poco forzados, como si se hubieran implementado y luego buscado una utilidad para los mismos. En la práctica se pueden utilizar para realizar uniones, intersecciones y diferencias entre conjuntos. Así como para obtener elementos aleatorios, incluso con repetición.

Sorted Set

Los conjuntos ordenados en Redis son colecciones de elementos a los que se puede asociar un valor o puntuación (score). Dicho valor se utiliza para ordenar los elementos retornados por las operaciones de consulta sobre los mismos.

El caso de uso más habitual es la implementación de listas con ranking (leader boards). Por ejemplo, para implementar una lista de jugadores con las mayores puntuaciones, u obtener la posición en el ranking de un jugador dado.

Comandos específicos para el tipo sorted set:

  • ZADD, añade elementos con puntuación
  • ZINCRBY, incrementa la puntuación de un elemento
  • ZSCORE, obtiene la puntuación de un elemento
  • ZRANK, obtiene el ranking de un elemento
  • ZREVRANK, obtiene el ranking de un elemento de forma descendente
  • ZPOPMIN, obtiene y elimina elementos con la menor puntuación
  • ZPOPMAX, obtiene y elimina elementos con la mayor puntuación
  • ZREM, elimina elementos
  • ZREMRANGEBYSCORE, elimina elementos en un rango de puntuaciones dado
  • ZREMRANGEBYRANK, elimina elementos en un ranking dado
  • ZREMRANGEBYLEX, elimina elementos en un rango lexicográfico dado
  • ZRANGE, obtiene elementos en un rango de puntuaciones dado
  • ZREVRANGE, obtiene elementos en un rango de puntuaciones dado ordenados de forma descendente
  • ZRANGEBYSCORE, obtiene elementos en un rango de puntuaciones dado ordenados por puntuación
  • ZREVRANGEBYSCORE, obtiene elementos en un rango de puntuaciones dado ordenados por puntuación de forma descendente
  • ZRANGEBYLEX, obtiene elementos en un rango de puntuaciones dado ordenados lexicográficamente
  • ZREVRANGEBYLEX, obtiene elementos en un rango de puntuaciones dado ordenados lexicográficamente de forma descendente
  • ZUNIONSTORE, obtiene la unión de varios conjuntos ordenados y la almacena en otro
  • ZINTERSTORE, obtiene la intersección de varios conjuntos ordenados y la almacena en otro
  • ZCARD, obtiene el número de elementos
  • ZCOUNT, obtiene el número de elementos en un rango de puntuaciones dado
  • ZLEXCOUNT, obtiene el número de elementos en un rango lexicográfico dado
  • BZPOPMIN, obtiene y elimina elementos con la menor puntuación de forma bloqueante
  • BZPOPMAX, obtiene y elimina elementos con la mayor puntuación de forma bloqueante
  • ZSCAN, itera por los elementos

En la práctica las puntuaciones se tratan ordenadas de menor a mayor, por lo que la mayoría de los comandos que recuperan rangos de elementos tienen un comando similar pero que recupera en orden inverso. En caso de dos elementos con la misma puntuación se retorna el resultado ordenado en base al orden lexicográfico de los elementos, que en Redis son siempre cadenas de texto. El orden está garantizado en la medida que un conjunto no puede contener dos veces una misma cadena.

El tipo sorted set es utilizado para casos de uso más elaborados, como por ejemplo para obtener productos similares a los consultados por un cliente en la web de una tienda, normalmente en función de las compras realizadas por otros usuarios que también consultaron o compraron los mismos productos.

HyperLogLogs

Este tipo está orientado a obtener el número de elementos distintos de una colección. Su peculiaridad es que no almacena realmente los elementos, por lo que no requiere mucha memoria, y no retorna un valor exacto, sino una aproximación.

Su caso de uso habitual es obtener la cuenta de las distintas ocurrencias de una determinada entidad. Por ejemplo, para obtener el número de las distintas direcciones IP que visitan una página web.

Comandos específicos del tipo HyperLogLogs:

  • PFADD, añade un elemento
  • PFCOUNT, obtiene el número aproximado de elementos
  • PFMERGE, realiza la unión de varios HyperLogLogs y la almacena en otro

Para el caso de uso habitual comentado, contar las distintas IPs que visitan una web, lo ideal sería llevar una cuenta a base de guardar todas las direcciones, incrementando el contador en el caso de que la dirección no estuviera ya guardada. Lógicamente, si se hiciera así, los requerimientos de memoria y tiempo de proceso serían inviables. La solución de Redis es hacer un hash por cada elemento (IP) añadido al conjunto (HyperLogLogs) y contar los distintos hashes. Esto elimina la necesidad de guardar todos los elementos y da una aproximación bastante buena de la cardinalidad (contador) del conjunto. De hecho, según la documentación, con un error de menos del 1%.

Streams

El tipo stream estará disponible a partir de la versión 5 de Redis. Su caso de uso más natural será el de modelar series temporales. Es decir, registrar información asociada a un determinado momento en el tiempo.

Es equivalente a un fichero de log, donde cada línea tiene un timestamp. Y aunque aún es pronto para afirmarlo, puede convertirse en el sistema de referencia para almacenar de forma local las medidas generadas por los sensores de dispositivos IoT (Internet of Things) antes de ser enviadas a la nube para su procesamiento.

Comandos específicos del tipo stream:

  • XADD, añade un elemento
  • XRANGE, obtiene elementos en un rango dado
  • XDEVRANGE, obtiene elementos en un rango dado en orden descendente
  • XLEN, obtiene el número de elementos
  • XREAD, obtiene elementos en un rango dado, opcionalmente de forma bloqueante
  • XREADGROUP, obtiene elementos en un rango para un grupo dado, opcionalmente de forma bloqueante
  • XPENDING, obtiene los elementos pendientes de un grupo

Los streams guardan elementos de igual forma que otras estructuras, pero su implementación está optimizada para su caso de uso más natural. Cuando se añade un elemento se genera automáticamente un ID que se compone de dos partes. La primera parte es un timestamp y la segunda es un secuencial dentro de dicho timestamp. Opcionalmente se puede utilizar un ID propio obviando el generado automáticamente, pero no se espera que sea una opción muy utilizada.

Redis ofrece dos formas de consultar los datos almacenados en un stream. La primera es mediante una consulta por rango, y la segunda es mediante un mecanismo de publicación/subscripción. Esta segunda forma se suma a los mecanismos ya existentes dentro de Redis, como los comandos bloqueantes para extraer elementos de listas, y los comandos más específicos como PUB/SUB para operar con canales. La novedad es que los clientes podrán suscribirse para obtener las entradas registradas a partir de un momento concreto en el tiempo. Con los tipos existentes anteriormente esto no era posible, cuando un cliente se desconectaba y volvía a conectarse no podía acceder a la información generada durante el periodo de desconexión.

En definitiva, un nuevo tipo añadido a Redis que aún está por venir y tiene más características por explotar, como los grupos de consumidores, que garantiza que cada mensaje es enviado a un único consumidor distinto dentro del grupo.

Redis (1)

La introducción de un nuevo tipo de datos en la futura versión 5 de Redis supone una buena oportunidad para escribir un par de artículos sobre este veterano software.

Aunque tradicionalmente vista como una base de datos NoSQL de tipo clave-valor en memoria, útil sólo como cache, en realidad Redis ofrece soluciones para un mayor número de casos de usos a través de sus componentes básicos: tipos y comandos.

Los tipos soportados por Redis son las cadenas de textos, arrays de bits, hashes, listas, conjuntos, conjuntos con peso, e HyperLogLogs (una estructura creada específicamente para estimar el número de elementos distintos de una colección). La futura versión 5 de Redis añadirá además los streams, un tipo especializado para la gestión de series temporal.

Los comandos de Redis son las herramientas a través de la que se construyen estructuras con los tipos soportados y se manipulan sus elementos. La integridad de los resultados está garantizada porque los comandos se ejecutan de forma atómica, pudiendo incluso utilizarse transacciones para ejecutar de forma atómica un conjunto de comandos y hasta ejecutar scripts en Lua.

La documentación de Redis es bastante detallada y su lectura obligada. Una característica de Redis a este respecto es que junto a la definición de los comandos se describen sus casos de uso más habituales, así como los posibles usos que se le puede dar a la herramienta, como por ejemplo su utilización como broker de mensajes mediante colas.

Instalación

No existe una distribución oficial de Redis para Windows, pero se pueden encontrar distribuciones no oficiales, o más simplemente utilizar una imagen de Docker.

En el caso de las distribuciones no oficiales hay que descomprimir el fichero descargado en un directorio y ejecutar redis-server.exe. El fichero de configuración por defecto es redis.conf. Y la forma más sencilla de probar la instalación es usando el cliente de línea de comandos ejecutando redis-cli.exe.

Comentar que Redis soporta de forma nativa su despliegue en una configuración distribuida en cluster con dos modos de persistencia, el modo RDB (Redis Database File), que funciona creando snapshots cada N unidades de tiempo, siempre y cuando se hayan modificado M claves en dicho periodo, y el modo AOF (Append Only File), que funciona almacenando todas las operaciones a fichero de forma inmediata garantizando la integridad de la información a costa de un peor rendimiento.

Claves

Redis almacena todos los datos asociados a una clave única, como las claves primarias en las base de datos relacionales, sólo que las claves en Redis son siempre cadenas.

No hay una forma estándar de escribir claves, sólo unas recomendaciones generales basadas en el uso de namespaces separados por el carácter de dos puntos. Por ejemplo, user:1000:views podría utilizarse para almacenar el número de veces que se ha visto el perfil del usuario con id 1000, pero no deja de ser una recomendación, no una obligación seguir dicho formato.

Todas las claves se tratan siempre en función de su representación a nivel binario. Es decir, no tiene sentido hablar de ningún tipo de codificación, como UTF-8, o juego de caracteres, como ISO-8859-1. Las claves se comparan siempre en base a la secuencia de bytes que las representan.

Comandos específicos para claves:

  • KEYS, obtiene las claves que siguen un patrón
  • SCAN, itera por todas las claves de la base de datos actual seleccionada
  • RANDOMKEY, obtiene una clave de forma aleatoria
  • EXISTS, comprueba si una clave existe
  • RENAME, renombra una clave
  • RENAMENX, renombra una clave, pero sólo la nueva clave no existe
  • DEL, borra una clave
  • UNLINK, borra una clave de forma no bloqueante
  • TYPE, obtiene el tipo del valor almacenado bajo una clave
  • SORT, ordena los elementos de una lista, conjunto o conjunto con pesos
  • EXPIRE, establece el tiempo de vida de una clave en segundos
  • EXPIREAT, establece el tiempo de vida de una clave con un timestamp
  • TTL, obtiene el tiempo de vida de una clave
  • PEXPIRE, establece el tiempo de vida de una clave en milisegundos
  • PEXPIREAT, establece el tiempo de vida de una clave con un timestamp en milisegundos
  • PTTL, obtiene el tiempo de vida de una clave en milisegundos
  • PERSIST, elimina el tiempo de vida de una clave
  • DUMP, obtiene una cadena resultado de serializar una clave
  • RESTORE, restaura una clave a partir de una cadena obtenida con DUMP
  • OBJECT, obtiene un volcado de características internas de una clave
  • TOUCH, modifica la fecha de último acceso a una clave
  • MOVE, mueve una clave a otra base de datos
  • MIGRATE, mueve una clave a otra instancia de Redis

Una característica interesante de Redis es que se puede programar un tiempo de expiración para una clave de forma individual, de forma que pasado ese tiempo Redis la eliminará de memoria automáticamente.

String

Las cadenas de texto son el tipo más básico de Redis. Como ya se ha comentado, es el tipo utilizado para almacenar las claves.

Su caso de uso más habitual es almacenar información a modo de cache. Por ejemplo, para almacenar tokens, cookies, fragmentos estáticos de HTML, o una página completa, utilizando su URL como clave y el HTML como valor.

Comandos específicos para el tipo string:

  • SET, establece el valor de una clave
  • SETNX, establece el valor de una clave, pero sólo si no existe
  • SETEX, establece el valor y tiempo de vida de una clave en segundos
  • PSETEX, establece el valor y tiempo de vida de una clave en milisegundos
  • GET, obtiene el valor de una clave
  • GETSET, establece el valor de una clave y retorna el valor anterior
  • MSET, establece el valor de una serie de claves
  • MSETNX, estable el valor de una serie de claves, pero sólo si ninguna existe
  • MGET, obtiene el valor de una serie de claves
  • INCR, incrementa el valor entero de una clave
  • INCRBY, incrementa el valor entero de una clave por un valor dado
  • INCRBYFLOAT, incrementa el valor decimal de una clave por un valor dado
  • DECR, decrementa el valor entero de una clave
  • DECRBY, decrementa el valor entero de una clave por un valor dado
  • STRLEN, obtiene la longitud del valor de una clave
  • GETRANGE, obtiene una subcadena del valor de una clave
  • SETRANGE, establece una subcadena del valor de una clave
  • APPEND, añade un valor al valor de una clave
  • BITFIELD, ejecuta comandos a nivel de campos de bits sobre el valor de una clave

Comentar que los valores de tipo string en Redis pueden tener una longitud de hasta 512 MB.

Bitmap

Redis no considera bitmap como un tipo, sino como un conjunto de comandos que permiten realizar operaciones a nivel de bit sobre valores del tipo string. Lo que cobra sentido cuando se recuerda que Redis trata las cadenas como secuencias de bytes, no como secuencias de caracteres.

El caso de uso habitual es marcar con un bit a 1 si alguna condición se cumple. Por ejemplo para indicar que el usuario de una aplicación web ha leído y aceptado las condiciones de uso de la misma. O para indicar si quiere recibir correos electrónicos periódicos con las novedades de la misma.

Comandos específicos para el tipo bitmap:

  • SETBIT, establece el valor de un bit
  • GETBIT, obtiene el valor de un bit
  • BITOP, realiza operaciones lógicas a nivel de bit
  • BITCOUNT, cuenta el número de bits a 1
  • BITPOS, obtiene el primer bit a cero o uno

Los bitmaps son adecuados además para señalizar eventos que ocurren con una periodicidad determinada y obtener estadísticas sobre ellos. Por ejemplo, para almacenar si un usuario visita una web cada día, utilizando un bit por día. Siendo lo más habitual distribuir esta información entre varias claves. Por ejemplo, guardando un bitmap por cada año, mes, o periodos más pequeños en función de cada evento concreto.