Ejemplo con Redis y Jedis

De ChuWiki
Saltar a: navegación, buscar

Veamos algunos ejemplos de Java conectado a Redis usando una librería sencilla de cliente, Jedis. En este enlace tienes el ejemplo de Redis y Jedis.

Dependencias

Se supone que hay un Redis arrancado en la IP 192.168.99.100 y puerto 6379. Esta IP y puerto son los que nos dan por defecto si arrancamos Redis en un contenedor Docker sobre Windows, para evitar tener que instalarlo.

El proyecto está montado como gradle. La única dependencia es

apply plugin: 'java'

repositories {
   mavenLocal()
   mavenCentral()
}

dependencies {
	compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
}

Hay otros clientes java, pero este es sencillote, aunque limitado.

Establecer conexión

Jedis no es thread-save, por lo que cada hilo que usemos debe usar su propia conexión. Una forma sencilla de obtener la conexión es instanciar la clase Jedis pasando IP y puerto

Jedis jedis = new Jedis("192.168.99.100",6379);

pero crear conexiones no suele ser buena idea, es mejor usar un Pool de conexiones, y Jedis nos proporciona uno. Hacemos una clase Pool para guardar el Pool de conexiones de Jedis y pedirle a él las conexiones cuando las necesitemos

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class Pool {
    private static JedisPool pool = new JedisPool("192.168.99.100",6379);

    public static Jedis getResource(){
        return pool.getResource();
    }
}

La forma de usarlo para asegurarnos que cerramos la conexión cuando no la necesitemos más y devolverla al Pool es la siguiente

        try (Jedis jedis = Pool.getResource()) {

           // Hacer cosas con la conexon jedis

        } catch (Exception e){
            e.printStackTrace();
        }

Esta forma es válida en java 8 y superior, try(abrir recurso) {...} se encarga automáticamente de cerrar el recurso cuando salimos del try. Si usamos java anterior a la 8, debemos usar un try-catch-finally tradicional y cerrar jedis, llamando a su método close(), en el finally.

Set y Get

En Redis podemos guardar con una clave String un valor String o byte[]. Si queremos guardar otro tipo de objetos, tendremos que convertirlo por el procedimiento que consideremos adecuado y la librería que más nos guste a String o byte[]. El método para guardar y recoger es bastante inmediato

jedis.set("foo", "bar");
String value = jedis.get("foo");

Simplemente un set con clave/valor y cuando lo necesitemos, incluso en otro programa java que esté conectado al mismo Redis, un get con clave para obtener el valor.

Listas

En redis tenemos varios comandos para añadir elementos a una lista y retirarlos. Podemos añadir y retirar por la derecha y por la izquierda, podemos insertar elementos en medio, retirarlos de enmedio, etc, etc. En el ejemplo, por un lado vamos metiendo elementos por la derecha (rpush) y vamos a ir retirándolos por la izquierda (lpop), de forma que el primero que metemos en la lista es el primero que sale. Para retirar, lo haremos de forma que la lectura quede bloqueada hasta que haya un elemento disponible.

Escribir

El código para meter datos en la lista puede ser como este

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;

public class ListWriterThread extends Thread {

    public ListWriterThread() {
        start();
    }

    public void run() {
        try (Jedis jedis = Pool.getResource()) {
            while (true) {
                double number = Math.random();
                jedis.rpush("list1", "ListWriter : " + Double.toString(number));
                Thread.sleep((long) (number * 1000));
            }
        } catch (Exception e){
            e.printStackTrace();
        }

    }
}

Hemos hecho un hilo que arrancamos y obtenemos una conexión Jedis. Luego, en un bucle infinito, generamos un número aleatorio que será el dato que queremos meter en Redis, en la lista "list1". Hacemos para ello una llamada a jedis.rpush(), para meter el dato por el lado derecho de la lista.

Luego una espera aleatoria de hasta un segundo, para no meter datos muy rápido y que no nos sature el log luego el lector de datos.

Leer

En cuanto a la lectura, el código es el siguiente

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;

import java.util.List;

public class ListReaderThread extends Thread {

    public ListReaderThread() {
        start();
    }

    public void run() {
        try (Jedis jedis = Pool.getResource()) {
            while (true){
                List<String> list = jedis.blpop(1,"list1");
                if (2==list.size()){
                    System.out.println("ListReader : "+list.get(1)); //list.get(0) gets the key
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Al igual que en el caso anterior, un hilo que arrancamos. Obtenemos conexión Jedis y en un bucle infinito hacemos la llamada a jedis.blpop(), que intentará leer un dato del lado izquierdo de la lista, pero se quedará bloqueada si no hay dato disponible. Lleva dos parámetros:

  • Un timeout en segundos. Hemos puesto un segundo. Un valor de 0 deja bloqueada la llamada indefinidamente, hasta que haya algún dato que retirar.
  • El nombre de la lista "list1". Podemos añadir más parámetros que serían los nombres de más listas. De esta forma, la llamada se quedaría bloqueada hasta que haya un dato disponible en cualquiera de las listas que pasamos como parámetro.

La llamada devuelve una List<String> con cero o dos elementos. Si salta el timeout, la lista devuelta tendrá cero elementos. Si hay un dato disponible, la lista tendrá dos elementos. El primer elemento es la clave de la lista que contiene el dato que hemos obtenido. En nuestro caso, este primer elemento será "list1", puesto que es la única lista que estamos leyendo. El segundo elemento será el dato en cuestión, así que lo sacamos por pantalla.

Hash

Escritura

Aunque en un Hash tradicionalmente se habla de clave y valor, aquí vamos a seguir la nomenclatura de Redis, ya que la palabra Clave puede dar lugar a equívoco al no saber si nos referimos a la clave con que Redis guarda el Hash o a la clave del Hash con la que se guarda un valor concreto. En Redis se usa clave para la clave de Redis donde se guarda el Hash completo y se llama Campo (Field) a la clave que se usa dentro del Hash para guardar un valor. Es decir, hablaremos de claves, campos y valores.

El código para meter valores en el Hash puede ser como el siguiente

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class HashSetThread extends Thread{

    public HashSetThread() {
        start();
    }

    public void run() {
        Random random = new Random();
        try (Jedis jedis = Pool.getResource()) {
            while (true) {
                double value = Math.random();
                String field = "Field"+random.nextInt(10);
                jedis.hset("hash", field, Double.toString(value));
                jedis.lpush("lastUpdatedField", field);
                Thread.sleep((long) (1000 * value));
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

Nuevamente un hilo que arrancamos en el constructor de la clase. La clave para el hash será "hash" y usaremos jedis.hset() para meter valores en ese Hash. El método jedis.hset() admite tres parámetros:

  • La clave que nos permite identificar el hash, usaremos "hash".
  • La clave que nos permite identificar el campo dentro del hash. Usaremos como campo "FieldN" donde N es un número aletatorio de 0 a 9 ambos incluidos
  • Y de valor un número aleatorio cualquiera.

También, en una lista de nombre "lastUpdatedField" iremos añadiendo el nombre del último campo modificado. Cuando hagamos el código de leer diremos el porqué.

Luego una espera aleatoria de hasta un segundo para no machacar a Redis metiendo datos a toda velocidad.

Lectura

Para leer el hash usaremos el siguiente código

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Random;

public class HashGetThread extends Thread{

    public HashGetThread() {
        start();
    }

    public void run() {
        Random random = new Random();
        try (Jedis jedis = Pool.getResource()) {
            while (true) {
                List<String> lastUpdatedField = jedis.blpop(1, "lastUpdatedField");
                if (2 == lastUpdatedField.size()) {
                    System.out.println("hash[" + lastUpdatedField.get(1) + "]: " + jedis.hget("hash", lastUpdatedField.get(1)));
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

Nuevamente un hilo que arrancamos en el constructor, obtenemos la conexión Jedis y un bucle infinito.

En Redis no hay forma de suscribirse al Hash de forma que sepamos qué campo ha cambiado para leerlo. Por ello recurrimos al artificio de al hacer un set en el hash, añadir el campo cambiando en una lista. Este lector en un bucle infinito y que se bloquea leyendo la lista, espera que alguien inserte en esa lista un campo que se cambie dentro del Hash. Ese es el motivo por en el que en el código de escribir en el hash, hemos añadido el nombre del campo modificado en una lista "lastUpdatedField". Este código de lectura espera una inserción en esa lista.

Con jedis.blpop(), que ya explicamos antes en las listas, leemos el nombre del campo cambiado. Una vez que lo tenemos, ya si hacemos un jedis.hget() para obtener el valor del campo.

Subscripción al Keyspace

Redis permite que nos suscribamos en general a cosas que pasan en Redis, en cocreto, a claves que cambian su contenido. Podemos filtrar el tipo de eventos que nos interesan y de esta forma podemos suscribirnos a cuando cambia el valor de una clave, o la lista de una clave o del hash de una clave. El problema es que nos enteramos cuando se añade, se reemplaza el contenido o se borran elementos, pero no nos enteramos quizás con el detalle que nos interesa. Por ejemplo, podemos enterarnos que han añadido un campo nuevo a un hash, pero no qué campo concreto.

El siguiente código muestra como suscribirnos a todo

package com.chuidiang.examples.jedis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class KeyspaceEventsSubscriptorThread extends Thread{

    public KeyspaceEventsSubscriptorThread() {
        start();
    }

    public void run() {
        try (Jedis jedis = Pool.getResource()) {

            jedis.configSet("notify-keyspace-events","KEA");

            jedis.psubscribe(new JedisPubSub() {
                @Override
                public void onPMessage(String pattern, String channel, String message) {
                    System.out.println("Keyspace Changes: " + pattern + " " + channel + " " + message);

                }

            }, "__key*__:*");
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

Un detalle, por defecto las notificaciones están desactivadas en Redis. Tenemos que cambiar la configuración de esta forma

jedis.configSet("notify-keyspace-events","KEA");

"KEA" es un conjunto de letras en el que cada letra tiene un significado y hay muchas otras posibles.

K     Keyspace events, published with __keyspace@<db>__ prefix.
E     Keyevent events, published with __keyevent@<db>__ prefix.
g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$     String commands
l     List commands
s     Set commands
h     Hash commands
z     Sorted set commands
x     Expired events (events generated every time a key expires)
e     Evicted events (events generated when a key is evicted for maxmemory)
A     Alias for g$lshzxe, so that the "AKE" string means all the events.

K o E son obligatorios, hay que poner uno de los dos. El primero dice que tenemos interés en eventos generales de Redis y el segundo que tenemos interés en eventos de claves de datos.

El resto de letras indica qué nos interesa en concreto: listas, valores, conjuntos, hash, datos que expiran por tiempo, etc, etc, etc. La A es el conjunto de todos los anteriores, así que "KAE" nos subscribe a todo de todo.

Para suscribirnos, debemos usar jedis.psubscribe(). La diferencia entre psubscribe() y subscribe() a secas es que el primero admite una máscara como clave, subscribiendonos así a muchas claves de un solo golpe, mientras que el segundo admite una clave y no un patrón, sólo nos subscribe a una clave concreta.

Como primer parámetro pasamos una clase hija de JedisPubSub, en la que sobre escribimos el método que nos interese, en este caso onPMessage(). Recibimos en este método la clave con la que nos hemos suscrito (el patrón), la clave que realmente ha cambiado y un valor que nos indica qué ha cambiado.

Como segundo parámetro ponemos un patrón de la clave a la que queremos suscribirnos. ¿A qué clave nos suscribimos?. Las claves que nos envía Redis para estos eventos pueden ser __keyspace@db__:xxx o __keyevent@db__:yyy donde db es el nombre de la "base de datos" de Redis, identificada por un número y que por defecto es 0. xxx o yyy son el nombre de la clave o del evento. Por ello, salvo que queramos algo concreto y afinemos más el patrón, "__key*__:*" es un buen patrón para recibir todos los eventos. La siguiente es una salida de nuestro programa

Keyspace Changes: __key*__:* __keyspace@0__:hash hset
Keyspace Changes: __key*__:* __keyevent@0__:hset hash
Keyspace Changes: __key*__:* __keyspace@0__:lastUpdatedField lpush
Keyspace Changes: __key*__:* __keyevent@0__:lpush lastUpdatedField

Veamos la tercera línea

  • __key*__:* Es el patrón al que nos hemos suscrito.
  • __keyspace@0__:lastUpdatedField La clave concreta que ha cambiado, "lastUpdatedField"
  • lpush Qué hemos hecho en esa clave, un lpush

Vemos en la cuarta línea que el evento está repetido. En un caso como keyspace, en el otro como keyevent. En el primero dice que a lastUpdateField se le ha hecho un lpush, en el otro que se ha hecho un lpush sobre lastUpdateField. Usaremos uno u otro según estemos interesados en una clave concreta, o en todas las operaciones de un tipo (todos los lpush en todas las listas, por ejemplo).

Enlaces