Cambiar el nivel de aislamiento de un datasource en WebSphere
Quiero comenzar este post con algo de teoría acerca de transacciones y problemas que pueden surgir en un entorno multiusuario, para luego explicarles como se puede establecer el nivel de aislamiento de un datasource en un servidor de aplicaciones WebSphere de IBM.
Si no les interesa la teoría pueden ir directamente a los pasos para configurar el isolation level de un datasource en WebSphere.
Qué es una transacción?
Una transacción en una serie de operaciones que se ejecutan como si fueran una sola operación atómica. Una transacción nos garantiza que todas las operaciones dentro de la misma se ejecutarán exitosamente o ninguna lo hará. Las transacciones se encargan de manejar fallos en la red y en los equipos de manera segura y elegante. Asimismo, permiten a múltiples usuarios compartir la misma información y garantizan que cualquier conjunto de actualizaciones serán escritas como un todo y que las mismas no interferirán con las actualizaciones de otros clientes.
Qué son las propiedades ACID
Cuando se utilizan transacciones de manera correcta, las operaciones siempre se ejecutarán dentro de un marco con 4 garantías. Estas 4 garantías son conocidas como las propiedades ACID de las transacciones.
- Atomicidad (Atomicity): garantiza que todas las operaciones se agrupen dentro de una misma unidad y actúen como si fueran una sola unidad de trabajo. Es decir que todas las operaciones deben ejecutarse exitosamente para que la actualización se realice, puesto que si ocurre un error el alguna de ellas no se actualizará nada.
- Consitencia (Consistency): garantiza que una transacción deje al sistema en un estado consistente luego de completarse. Un estado consistente es aquél en el que se satisfacen las reglas de negocio.
- Aislamiento (Isolation): permite que múltiples transacciones lean o escriban en la base de datos sin tener conocimiento de las demás transacciones debido a que las mismas se encuentran aisladas una de las otras. Esto es útil en los casos en que múltiples clientes quieran actualizar la base de datos al mismo tiempo. De esta manera, cada cliente cree que es el único que está actualizando la base en ese instante. El sistema de transacciones logra el aislamiento usando protocolos de sincronización de bajo nivel en la base de datos. Esta sincronización es la que aísla el trabajo que se realiza en una transacción del trabajo de otra. Durante una transacción, los bloqueos son asignados automáticamente a medida que son requeridos. Si una transacción tiene un bloqueo de datos, éste previene que otras transacciones concurrentes no pueden interactuar con dichos datos hasta que el bloqueo sea liberado. Los bloqueos garantizan que otras actualizaciones concurrentes no interfieran mientras una transacción se está llevando a cabo.
- Durabilidad (Durability): garantiza que las actualizaciones de los recursos gerenciados, como por ejemplos los registros de la base de datos, sobrevivan a fallas. Las fallas pueden ser en los equipos, caídas de la red, discos rígidos o en el suministro de energía. Los recursos recuperables mantienen un historial de transacciones con dicho propósito. Si un recurso falla, la información puede ser reconstruida reaplicando estos pasos del historial.
Qué es el nivel de aislamiento o isolation level?
Es un mecanismo usado para aislar cada una de las transacciones en un entorno multiusuario. Si establecemos un nivel de aislamiento demasiado restrictivo, podemos llegar a tener problemas de performance debido a que las filas de la base de datos se encuentran bloqueadas. Por otro lado, un nivel de aislamiento demasiado permisivo puede derivar en problemas de integridad en los datos.

El uso adecuado de los mecanismos de niveles de aislamiento previene que las aplicaciones introduzcan errores en alguna de las siguientes formas:
Supongamos que hay 2 instancias de un mismo componente que se están ejecutando concurrentemente, probablemente en 2 procesos o en dos threads (hilos) diferentes. Asumamos que el componente quiere actualizar una base de datos compartida usando la API de JDBC o SQL/J. Cada una de las instancias realizará los siguientes pasos:
1. Leer un entero X desde la base de datos.
2. Sumar 10 a X.
3. Escrbir el Nuevo valor de X en la base de datos.
Todo será correcto si estos 3 pasos se ejecutan juntos en una operación atómica, ya que ninguna de las instancias interferirá una con la otra. Sin embargo, el algoritmo de thread-scheduling usado no nos garantiza esto. Si 2 instancias están ejecutando estas operaciones, las mismas pueden intercalarse. Este sería un orden posible:
1. La instancias A lee un entero X de la base de datos. La base tiene ahora X=0.
2. La instancia B lee un entere X de la base de datos. La base tiene ahora X=0.
3. La instancia A suma X a su copia de X y guarda X en la base. Ahora la base tiene X=10.
4. La instancia B suma 10 a su copia de X y la guarda en la base. Ahora la base tiene X=10.
Que ocurrió aquí? Debido a que las operaciones en la base de datos se intercalaron, la instancia B está trabajando con una copia vieja de X: la copia de antes que A haya escrito en la base. Por lo tanto, todas las operaciones realizadas por la instancia A se perdieron! Este problema es conocido como lost update (actualización perdida). Por lo tanto, como podemos evitar esto usando transacciones?
La solución a este problema es usar bloqueos (locks) en la base de datos para prevenir que los 2 componentes lean los datos al mismo tiempo. Al bloquear los datos que usa la transacción, garantizamos que sólo dicha transacción tendrá acceso a los datos hasta que el bloqueo sea liberado. De esta manera prevenimos que se intercalen operaciones en datos que son importantes.
En nuestro ejemplo, si el componente adquiere un bloqueo exclusive antes que comience la transacción y libera dicho bloqueo luego que finalice la transacción, podemos asegurar que no habrá ninguna intercalación.
1. Obtener un bloqueo en X.
2. Leer un entero X de la base de datos.
3. Sumar 10 a X.
4. Escribir el Nuevo valor de X en la base.
5. Liberar el bloqueo en X.
Si algún otro componente se ejecuta concurrentemente con éste, ese componente tendrá que esperar hasta que el bloqueo sea liberado, y de esta forma tendrá una copia actualizada de X.
Es importante que entendamos cuando pueden ocurrir los problemas de dirty reads, unrepeatable reads, and phantoms, ya que de otra manera no podremos manejar correctamente transacciones. Ahora veremos como hacer una correcta elección del nivel de aislamiento cuando programamos con transacciones.
El problema del Dirty Read
Un dirty read ocurre cuando una aplicación lee datos de una base da datos que aún no ha sido cometida a almacenamiento permanente. Consideremos 2 instancias de un mismo componente realizando lo siguiente:
1. La instancia A lee X de la base de datos. La base tiene X=0.
2. La instancia A suma 10 a X y la guarda en la base. La base tiene X=10. Aún no hicimos el commit, por lo tanto la actualización todavía no es permanente.
3. La instancia B lee X de la base. El valor que lee es X=10.
4. La instancia A aborta la transacción, por lo que ahora tenemos X=0 en la base.
5. La instancia B suma 10 a X y la guarda en la base, con lo que X=20 en la base.
El problema es que la instancia B lee la actualización realizada por la instancia A antes de ser cometida. Sin embargo, debido a que la instancia A abortó la transacción, el valor de X en la base de datos fue establecido incorrectamente en 20, aún cuando la instancia A haya abortado. Este problema de leer datos que aún no han sido cometidos es conocido como dirty read.
READ UNCOMMITTED
Los dirty reads pueden ocurrir cuando usamos en nivel de aislamiento más débil, llamado READ UNCOMMITTED. Supongamos que tenemos una Transacción 1(Tx1) cuyo nivel de aislamiento es READ UNCOMMITED, y otra Transacción 2(Tx2) cuyo nivel de aislamiento puede ser cualquiera. Si ambos se están ejecutando concurrentemente y Tx2 escribe datos en la base sin cometerlos, Tx1 leerá dichos datos, lo cual es incorrecto. Esto ocurrirá independientemente del nivel de aislamiento usado por Tx2. El nivel READ UNCOMMITTED también puede experimentar otros problemas tales como unrepeatable reads y phantoms.
Cuándo usar READ UNCOMMITTED?
Este nivel no debe usarse en sistemas de misión críticos con datos compartidos que están siendo actualizados por transacciones concurrentes. No es apropiado usarlo en cálculos sensibles, tales como transacciones bancarias.
Este nivel es apropiado en aquellas situaciones en que conocemos de antemano que la instancia de nuestro componente será ejecutada sin transacciones concurrentes. Sin embargo este nivel no es recomendado para la mayoría de las aplicaciones que usan transacciones. La ventaja de este nivel es la performance, ya que no hay que obtener bloqueos de datos compartidos.
READ COMMITTED
Este nivel es muy similar a READ UNCOMMITTED, con la diferencia que el sistema no leerá datos que fueron guardados pero no cometidos. Por lo tanto solucionamos el problema del dirty read, sin embargo este nivel no nos protege de otros problemas más complejos tales como unrepeatable reads y phantoms.
Cuándo usar READ COMMITTED?
Este nivel nos garantiza que no leeremos datos que no han sido cometidos. Por lo tanto puede usarse en aplicaciones que leen información de la base datos y muestran reportes. Debido a que en este nivel es necesario adquirir bloqueos, la performance es inferior al nivel READ UNCOMMITTED. READ COMMITTED es el nivel de aislamiento por defecto en la mayoría de las bases datos, tales como Oracle y Microsoft SQL Server.
El problema del Unrepeatable Read
Los unrepeatable reads ocurren cuando un componente información de la base de datos, pero cuando vuelve a leer la información, ésta ha cambiado. Esto puede ocurrir cuando otra transacción que se está ejecutando concurrentemente modifica los datos que están siendo leídos. Por ejemplo:
1. Nuestra aplicación lee un conjunto de datos X de la base de datos.
2. Otra aplicación sobrescribe ese conjunto X con otros valores.
3. Volvemos a leer el conjunto de datos X de la base. Los valores cambiaron mágicamente!
Sin embargo, usando bloqueos en transacciones para prevenir que otras modifiquen los datos mientras estos son leídos, podemos garantizar que nunca ocurra un unrepeatable read.
REPEATABLE READ
El nivel de aislamiento REPEATABLE READ garantiza una propiedad más que el READ COMMITTED: siempre que leamos datos cometidos desde la base datos, seremos capaces de volver a leer dichos datos más tarde, y éstos tendrán el mismo valor que tenían la primera vez. Así, las lecturas de la base de datos son repeatables(repetibles).
Cuándo usar REPEATABLE READ?
Usamos REPEATABLE READ cuando necesitamos actualizar uno o más registros de una base de datos. En este caso queremos leer cada una de las filas que estamos modificando y luego ser capaces de actualizar cada fila, sabiendo que ninguna de ellas está siendo modificada por otra transacción que se está ejecutando concurrentemente. Si luego decidimos leer nuevamente cualquiera de las filas dentro de la transacción, tendremos garantizado que los datos de filas serán los mismos que al inicio de la transacción.
El problema Phantom Read
Un phantom es un nuevo conjunto de datos que aparece mágicamente en la base de datos entre dos operaciones de lectura. Por ejemplo:
1. Nuestra aplicación hace una consulta a la base de datos y obtiene un conjunto de registros.
2. Otra aplicación inserta un registro en la base, el cual cumple con la condición de nuestra consulta. 3. Ejecutamos nuevamente la consulta y vemos que un nuevo registro apareció.
La diferencia entre el problema unrepeatable read y el phantom es que el primero ocurre cuando se modifican los datos existentes, mientras que el segundo ocurre cuando se insertan datos que antes no existían.
SERIALIZABLE
Usando este nivel de aislamiento es fácil evitar éste y los otros problemas descriptos anteriormente. SERIALIZABLE es el nivel más estricto y garantiza que las transacciones se ejecutan en forma serializada respecto de las demás, satisfaciendo las propiedades ACID. Esto significa que cada transacción es realmente independiente una de otra.
Cuándo usar SERIALIZABLE?
Se debe usar en sistemas de misión crítica que deben tener un perfecto aislamiento transaccional. De esta forma garantizaremos que no se leerán datos que no fueron cometidos, que podremos leer lo mismos datos una y otra vez, y que no aparecerán nuevos datos misteriosamente debido al uso de transacciones concurrentes.
Este nivel debe usarse con precaución puesto que tiene un alto costo en la performance.
Entonces, cómo especificamos el nivel de aislamiento para un datasource en WebSphere?
Perdón si la explicación fue un poco extensa, pero si has leído todo el post comprenderás mejor cuál es el nivel de aislamiento que deberás utilizar en tu aplicación. En mi caso particular, el problema se presentó con una aplicación que utilizaba un datasource para conectarse a una base de datos Informix. Por defecto, Informix establece en nivel de aislamiento en REPEATABLE READ, lo que hace que la performance caiga notablemente, por lo tanto debía cambiarlo a READ COMMITED.
Para establecer el nivel de aislamiento de un datasource en un servidor WebSphere hay que utilizar la propiedad webSphereDefaultIsolationLevel, la cual se encuentra disponible a partir de WebSphere V5.1.1.6 y V6.0.2.

Los pasos a seguir son estos:
- Ir a la opción Resources > JDBC provider.
- Seleccionar el JDBC Provider.
- Seleccionar el DataSource.
- Hacer click en el nombre del DataSource al cual le queremos cambiar el nivel de aislamiento.
- Buscar donde dice additional properties y hacer clikc en Custom properties.
- Hacer click en New para agregar una nueva propiedad, en el campo nombre ingresar webSphereDefaultIsolationLevel y en el campo value ingresar el valor correspondiendo (1, 2, 4, 8).
- Hacer click en OK y guardar la configuration.
- Reiniciar el Application Server para activar esta propiedad.
Vieron que simple era no?
Más datos en IBM WebSphere App Server
Si te gustó el post, podés dejar tu comentario o suscribirte al feed para recibir los últimos artículos en tu email.


Comentarios
Ningún comentario.
Deja tu comentario