Transacciones y bloqueos en SQL-Server (Información para el uso desde Axapta) Introducción En este documento vamos a intentar explicar cuatro conceptos básicos acerca de las transacciones y los bloqueos en SQL-Server y lo más importante: como los evita/gestiona Axapta cuando puede. Y cuando no puede algún truquillo para ayudarle. Transacciones Si a mitad de ejecutar un proceso se produce un error, no podemos asegurar la integridad de los datos actualizados por nuestra aplicación. Es decir, si teníamos que actualizar 500 registros de la Base de datos y se ha producido un error cuando habíamos procesado tan solo 200 los otros 300 quedan sin actualizar. En muchos ocasiones esto no es para nada deseable (imaginemos una facturación, se nos produce un error a mitad de procesar los pedidos incluidos en una factura y quedan pedidos marcados como facturados y otros que no y la factura podría no haberse completado). Para evitar este problema existen las transacciones. Una transacción es un sistema que utilizan las BBDD para saber cuando un proceso termina satisfactoriamente. Podríamos decir que es un todo o nada. El tema consiste en que nuestro proceso realiza las actualizaciones que necesita en la base de datos y cuando ha terminado todo el proceso le indica al SQL que todo lo que ha realizado puede darlo por bueno. Por el contrario si hay algún problema, la base de datos dispone de un mecanismo que le permite anular todos los cambios realizados en los datos durante ese proceso. En Axapta, para saber que actualizaciones de la BBDD pertenecen al mismo lote deberemos indicar donde empieza y termina cada transacción: ttsbegin; // principio de la transacción // instrucciones de actualización ttscommit; // final de la transacción
Si no se produce ningún error, se ejecutará la instrucción de fin de transacción y la BBDD guardará las actualizaciones que se han realizado dentro de la transacción. En cambio, si existiese un error se produciría un RollBack que consiste en anular todas las actualizaciones realizadas desde el inicio de la transacción. El famoso RollBack se ejecuta automáticamente en caso de producirse una excepción, pero también podemos ejecutarlo de forma intencionada mediante la instrucción ttsabort. Por supuesto podemos anidar transacciones de manera que se realizará el commit real en la base de datos cuando lleguemos al ttscommit de la transacción de nivel superior. ttsbegin;...... ttsbegin; ttscommit; // Aquí aun no se realiza el commit realmente ttscommit; // Ahora si :) A medida que el interprete de Axapta va encontrando instrucciones ttsbegin va incrementando la variable global ttslevel y por cada ttscommit lo decrementa. Axapta comunica a la base de datos el commit real cuando ttslevel llega a 0. Para los más curiosos tan solo decir que además podéis echar un vistazo a la clase Application, métodos ttsnotifybegin, ttsnotifyabort y ttsnotifycommit. Estas se llaman cada vez que ejecutamos un ttsbegin, ttsabort o ttscommit respectivamente para que Axapta realice una serie de notificaciones a dos sistemas de transacciones especiales de que dispone este ERP. Bloqueos Cuando se esta ejecutando simultáneamente mas de un proceso, a veces sucede que los dos intentan modificar el mismo registro.
Debido al uso de transacciones el sistema puede encontrarse con el problema de que un proceso (A) intenta modificar o leer un registro que ha sido modificado por otro proceso (B) cuya transacción no ha finalizado. El problema está en que podría darse el caso de que esa transacción no terminara de forma satisfactoria (commit) sino que tuvieran que anularse los cambios producidos en ese registro. Que se supone que debería hacer SQL cuando intentamos leer ese registro? Mostrar el valor anterior a la actualización producida por el proceso que está en curso (B)? Mostrar el valor modificado por el proceso (B)? En cualquiera de los 2 casos, y si falla el proceso (B), que valor dejamos en el registro cuando realicemos el rollback? Dejamos el valor anterior a la actualización producida por el proceso (B) a riesgo de que el proceso (A) haya modificado también este registro y machaquemos sus cambios? Imaginemos que se trata de un acumulado (del stock por ejemplo): El stock era 5, el proceso (B) ha restado una unidad y lo ha dejado a 4. El proceso (A) quería restar 2 unidades y basándose en el 4 lo ha dejado a 2. Si falla el proceso (B) y restaura el valor a 5 acabamos de destrozar el acumulado. Para solucionar este problema Sql-Server generará un bloqueo en el momento en que el proceso (B) modifique el registro de manera que el proceso (A) esperará a que (B) termine (bien o mal) para poder continuar. De esta forma nos aseguramos de que el proceso (A) se basará en datos correctos y no pondremos en peligro la consistencia de los mismos. DeadLocks Imaginemos que el proceso (A) bloquea el registro nº1, el proceso (B) bloquea el registro nº2. Ahora el proceso (A) que prosigue su trabajo se dispone a modificar el registro nº2, pero encuentra que esta bloqueado por el proceso (B). A su vez, el proceso (B) (por aquellas cosas del destino y de Murphy) se dispone a proseguir su trabajo pero necesita modificar el registro nº1, que ah! Sorpresa! Está bloqueado por el proceso (A). En resumen : El proceso (A) está esperando a que finalice el proceso (B) para poder modificar el registro nº 2 y el proceso (B) está esperando a que termine el proceso (A) para poder modificar el registro nº1. Estamos frente a lo que se denomina DeadLock o lo que es lo mismo un bloqueo sin solución. Normalmente Sql-Server detectará el problema, elegirá uno de los dos procesos y lo cancelará. A veces no es capaz de detectarlo y se quedan bloqueados hasta que el usuario, el administrador o alguna otra circunstancia cancele uno de los procesos. Una posible solución sería limitar el tiempo de espera para un bloqueo mediante el parámetro de SQL Lock_Timeout. El problema que nos podría ocasionar es que en el
caso que no se tratase de un DeadLock si no de una espera un poco larga pero con final feliz, también generaría un error y cancelaría la transacción. Si nos fijamos, veremos que para evitar que dos procesos tengan que modificar el mismo registro, en Axapta casi no existen acumulados y contadores (secuencias numéricas), y decimos casi porque hay casos en los que no tiene mas remedio que usarlos, pero en estos casos tienen una gestión especial para evitar DeadLocks. Stocks Los stocks de inventario son un caso claro de acumulados. A partir de la Versión 3.0 de Axapta se ha añadido una funcionalidad llamada "transacciones múltiples de Inventario". Cuando está activada en lugar de acumular los movimientos de inventario directamente en el stock (tabla InventSum), los guarda en una tabla como operaciones pendientes de aplicar. Si la transacción termina correctamente realiza la actualización del stock en los acumulados correspondientes. ilustración 1: En el menú Administración, Configurar, Sistema se encuentra esta opción que permite activar o desactivar esta funcionalidad.
Secuencias Numéricas Para evitar los bloqueos, Axapta actualiza los contadores creando una transacción en una conexión independiente, de esta forma no bloquea la secuencia numérica hasta que termina todo el proceso (sería un caos). Por este motivo si una secuencia numérica es de tipo continua (que no debe perder números) el sistema almacena en una tabla a parte una lista de números pendientes de determinar si se han asignado definitivamente o no. Si la transacción finaliza correctamente estos números quedarán marcados como asignados. Escalado de Bloqueos SQL-Server mantiene una lista de los registros que se encuentran bloqueados en cualquier instante por todos los procesos que hay en ejecución. Esto puede consumir bastante memoria. Para evitar el consumo excesivo de memoria, Sql-Server dispone de un mecanismo llamado Escalado de bloqueos. Este consiste en que en un momento determinado, SQL decide que si hay muchos registros bloqueados de una tabla, puede cambiar la lista de referencias a cada uno de esos registros por una sola referencia a nivel de página o bien a nivel de tabla según el caso. Realmente puede ir escalando esta referencia incluso hasta llegar a nivel de Base de datos. Esto supone que puede bloquear toda una página o toda una tabla o toda la base de datos, lo que implica que por ahorrar consumo de memoria puede bloquear registros que no necesitaban ser bloqueados ya que no se han actualizado pero por desgracia se encontraban en una de las páginas que ha decidido
bloquear o en la tabla etc.(hay que señalar que el caso de bloqueo de Base de datos es un caso muy extremo y no suele darse) Este sistema a veces produce resultados indeseados. Por ejemplo en Axapta la tabla de pedidos de venta es común para todas las empresas. Por lo tanto si una de ellas estuviese realizando un proceso de facturación, sería posible que las demás no pudiesen modificar pedidos. Esto puede suceder si se tratan de facturas con muchas líneas, ya que Axapta crea una transacción para cada factura. Para evitar este tipo de bloqueos lo mejor es realizar transacciones pequeñas. Siempre y cuando sea posible (que en muchas ocasiones no lo es). Esto tiene truco Podemos desactivar el escalado de bloqueos en SQL-Server, existe un parámetro para hacerlo. Para introducirlo iremos a las propiedades de Sql-Server, solapa "General", Botón "Parámetros de Inicio", y añadimos el parámetro " T1211". De todas formas debemos ser conscientes de lo que esto supone, estamos privando al servidor de esta funcionalidad, mediante la cual consigue liberar memoria cuando la necesita, lo que podría degradar el rendimiento. Aunque en sistemas con muchos usuarios y transacciones simultaneas puede acabar siendo necesario. También parece ser que es mejor disponer de un índice con clave única para que Sql- Server pueda hacer referencia a los registros bloqueados. Si no es posible definir una clave única, sería buena solución crear un índice por el "RecId".