Ejemplos sencillos con Hazelcast

De ChuWiki

Veamos algunos ejemplos sencillos con Hazelcast

Dependencia maven[editar]

Necesitamos en nuestro proyecto java los jar de Hazelcast. Si usas una herramienta como maven, sólo tienes que poner en el fichero pom.xml la dependencia

		<dependency>
			<groupId>com.hazelcast</groupId>
			<artifactId>hazelcast</artifactId>
			<version>3.3.2</version>
		</dependency>


Un ejemplo básico[editar]

En el siguiente trozo de código hacemos lo mínimo imprescindible para obtener un Map de Hazelcast.

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
....
Config config = new Config();
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
       
IMap<String,String> map = hazelcastInstance.getMap("MapName");
map.put("SomeKey", "SomeValue");

Veamos qué hemos hecho y algunos detalles.

  • Instanciamos una configuración por defecto de Hazelcast con new Config()
  • Creamos una instancia de Hazelcast usando esa configuración con Hazelcast.newHazelcastInstance(config);
  • Le pedimos a Hazelcast que nos cree un Map de Java con hazelcastInstance.getMap("MapName");. Esta llamada tiene varios puntos interesantes que tenemos que tener en cuenta:
    • Le damos un nombre al Map, que es el texto "MapName" que estamos pasando como parámetro a la llamada. El nombre puede ser el que queramos. Si no hay un Map con ese nombre, Hazelcast lo crea y nos lo devuelve. Si hay un Map con ese nombre, nos devuelve el que ya existe. Esto nos permitirá compartir el mismo Map fácilmente en distintos sitios del código o incluso en ejecutables distintos que estén en servidores distintos (lo vemos más adelante).
    • La llamada devuelve una interfaz IMap, específica de Hazelcast. Esta interface hereda de la interfaz estándar de java Map, por lo que si no vamos a usar cosas específicas de Hazelcast, podríamos guardar el map en una variable de tipo Map y podríamos usarla en cualquier código o api ya existente de java que utilice Map.
  • Una vez obtenido el map, podemos usarlo de forma normal, añadiendo datos con put(), leyéndolos con get(), borrándolos con remove() y en definitiva cualquier método típico de la interfaz Map de java.

Hazelcast distribuido[editar]

Sin necesidad de hacer nada, con la configuración por defecto, Hazelcast busca instancias de él mismo dentro de la red local, usando multicast. Si dentro de la misma red local arrancamos el ejemplo anterior y en otro servidor arrancamos, después, un código como este

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
....
Config config = new Config();
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
       
IMap<String,String> map = hazelcastInstance.getMap("MapName");
String value = map.get("SomeKey");
System.out.println("value = "+value);

obtendríamos en este segundo ejecutable el valor "SomeValue" que introdujimos en el primer ejecutable. La forma de ver que ambos ejecutables se han conectado correctamente por Hazelcast, aparte de ver que el ejemplo funciona, es ver que la salida del programa muestra en su log

Members [2] {
	Member [192.168.1.2]:5701 this
	Member [192.168.1.3]:5701
}

donde se ve que hay dos miembros formando el cluster de Hazelcast.

En la configuración podríamos deshabilitar este "auto descubrimiento" por medio de multicast y poner las IPs específicas que queremos que formen parte de nuestro cluster

// Configuración por defecto
Config config = new Config();

// Obtenemos la configuración por defecto de la red
NetworkConfig network = config.getNetworkConfig();

// Deshabilitamos multicast
JoinConfig join = network.getJoin();
join.getMulticastConfig().setEnabled( false );


// Habilitamos la configuración por TCP con IPs fijas
join.getTcpIpConfig().addMember( "10.45.67.32" ).addMember( "10.45.67.100" )
            .setEnabled( true );

Esto es solo un pequeño ejemplo, hay muchas más opciones, como indicar miembros que deben estar obligatoriamente, rangos de IPs que se admite que unan al cluster, etc, etc. Aquí puedes ver los detalles de la configuración de red

Cuando tenemos una colección de Hazelcast distribuida, no todos los datos están en todos los servidores, Hazelcast los reparte. Sí están accesibles, desde cualquier servidor puedo consultar cualquier dato, porque Hazelcast se lo traerá del servidor donde esté realmente ese dato. La ventaja de este mecanismo es que poniendo varios servidores, podríamos guardar colecciones de dato más grandes que la memoria disponible de un solo servidor.


Time To Live[editar]

Para key/value concretos[editar]

Hazelcast permite que los datos que se guarden en las distintas colecciones tengan un tiempo de vida, es decir, una vez guardado el dato y pasado un determinado tiempo, ese dato desaparece automáticamente. Sólo tenemos que añadir el dato pasando dicho tiempo, como en el siguiente ejemplo

IMap<String,String> map = hazelcastInstance.getMap("TimeToLiveMap");       
map.put("hola", "tu", 10, TimeUnit.SECONDS);

En el método put() hemos pasado, además de clave y valor, dos parámetros adicionales, uno es el tiempo que queremos que ese dato permanezca en el map, es decir 10, y el otro parámetro es qué unidades tiene ese 10, en este caso, 10 segundos. Tras meter el dato, a los 10 segundos desaparece automáticamente y ya no está accesible.

La utilidad de este tipo de datos que desaparecen es como caché. Imagina que varios clientes se conectan a tu servidor y les da por pedir a tu base de datos el mismo dato. Una forma de evitar el acceso a la base de datos varias veces para conseguir el mismo dato es seguir el siguiente procedimiento:

  • Se mira si el dato está en la colección de Hazelcast.
  • Si no está, se consulta a base de datos, se guarda en Hazelcast con un cierto tiempo de vida, y se devuelve al cliente.
  • Si el dato sí está en Hazelcast, de devuelve directamente al cliente sin necesidad de pedirlo a base de datos.
  • Si un segundo cliente pide el mismo dato otra vez y no ha pasado el tiempo de caducidad del dato, le devolveremos el que está en Hazelcast (es justo el punto anterior).

Por supuesto, este mecanismo sólo sirve si los datos en base de datos no cambian muy rápido y si no es de suma importancia que el cliente obtenga el último dato al instante. Es ideal para peticiones a servidores web, donde varios usuarios pueden estar consultando la misma página y viendo los mismos datos.

Para el Map completo[editar]

Si todos los datos van a tener siempre el mismo tiempo de vida, es posible crear el Map de forma que ya esté configurado de esta manera. Así no es necesario que indiquemos en cada put el tiempo de vida.

El código para hacer esto sería el siguiente

hazelcastInstance.getConfig().addMapConfig(new MapConfig("TimeToLiveMap").setTimeToLiveSeconds(1));
IMap<String,String> map = hazelcastInstance.getMap("TimeToLiveMap");


La idea es añadir un MapConfig en la configuración de Hazelcast. Se indica el nombre del Map, "TimeToLiveMap" en nuestro ejemplo y el tiempo de vida en segundos, uno en nuestro ejemplo. Luego se pide el Map y ya se puede trabajar con él. Si en los put no se pone un tiempo de vida, por defecto será un segundo. Si se pone algún tiempo de vida, se hará caso al del put

Es importante configurar el Map antes de pedirlo en cualquier sitio con getMap()

Suscripción[editar]

Hazelcast permite que nos suscribamos a cambios en las colecciones (Map, List, etc). Un pequeño ejemplo de cómo hacerlo

HazelcastInstance hazelInstance = Hazelcast.newHazelcastInstance();
IList<ChatData> chatRoom = hazelInstance.getList("chatRoom");
chatRoom.addItemListener(new ItemListener<ChatData>() {
         
   public void itemRemoved(ItemEvent<ChatData> arg0) {
       // Nada que hacer
   }
         
   public void itemAdded(ItemEvent<ChatData> arg0) {
      chatArea.append(arg0.getItem().toString()+"\n");
   }
 }, true);

Imagina una sala de chat donde los clientes van guardando las conversaciones en una List de Hazelcast. La forma de enterarse de lo que dicen los otros para poder pintarlo en nuestra pantalla puede ser suscribirse a cambios en esa lista, en concreto, a inserciones nuevas.

Para ello, una vez que obtenemos de hazelcast la lista chatRoom, nos suscribimos por medio del método addItemListener(). A este método se le pasa un ItemListener que tiene dos métodos, uno para avisarnos cuando se borre un elemento, otro para avisarnos cuando se cree un elemento. En este último caso, añadimos el nuevo texto a donde vayamos a pintarlo (un chatArea, en el ejemplo del código).

Operaciones atómicas[editar]

Hazelcast ofrece la opción de hacer "operaciones atómicas", es decir, la opción de ver un valor almacenado en Hazelcast y modificarlo si no nos gusta en un solo paso, sin posibilidad de que otro proceso modifique ese valor justo entre que vemos su valor y lo modificamos.

Por ejemplo, imagina que arrancamos nuestro ejecutable en varios servidores y que sólo uno de ellos debe hacer el papel de maestro, mientras que todos los demás deben ser esclavos. ¿Cómo pueden decidir ellos de forma automática, sin que nosotros lo configuremos, cual hace de maestro?. Una opción es usar una operación atómica de Hazelcast, como se muestra en el siguiente código

HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
       
IAtomicLong atomicLong = hazelcastInstance.getAtomicLong("soy productor");
       
boolean cambiado = atomicLong.compareAndSet(0, 1);
       
if (cambiado){
   // soy el maestro
} else {
   // soy esclavo
}

Primero se obtiene un IAtomicLong, que es un Long con métodos para hacer dos operaciones de forma atómica (como si fuera solo una sin posiblidad de que nadie interfiera justo entre las dos operaciones). La operación que nos interesa es comparar con cero y si es cero, darle el valor uno. Para ello se usa el método atomicLong.compareAndSet(0, 1), que nos devolverá true si la operación se lleva a cabo (es decir, el valor es cero y lo incrementamos a uno), o false si no se lleva a cabo (es decir, el valor no es cero y por tanto no se incrementa a uno).

Si todos nuestros ejecutables hacen esto, sólo el primero que lo intente encontrará que el valor es cero y podrá incrementarlo a uno. Es decir, sólo al primero se le devolverá true en esta llamada y decidimos que ese sea el maestro. A los demás procesos se les devolverá <false> en esta llamada, puesto que el valor ya ha sido cambiado a uno, y harán de esclavos.

La necesidad de que la operación comparar y cambiar el valor sea atómica es para evitar que un proceso compare con cero y antes de que le de tiempo a cambiar el valor a uno, un segundo proceso también compare con cero, con lo que tendríamos dos maestros.

Enlaces[editar]