AndorraDotNet

Comunitat d'usuaris .NET del pais dels pirineus
¿No perteneces a AndorraDotNet? ¡Regístrate y sé un miembro!
Iniciar sesión | REGÍSTRATE AQUÍ | Ayuda
en Buscar

400 Bad Request

Dímelo con flores ;-)

November 2007 - Artículos

  • La evolución del contexto transaccional

    ** Artículo original publicado en DotNetMania Nº35 **
    El amigo

    Me comentaba mi amigo y colega Marino Posadas en el pasado MVP Open Day, lo interesante que sería que algunos de nosotros se propusieran escribir un artículo para dotnetsolidario.com, y así al mismo tiempo efectuar una buena acción en estas fechas señaladas. Embriagado por el espíritu navideño y como oportunidades de este tipo no abundan, en seguida entré en un estado de sopor-concentración en busca de la inspiración necesaria para afrontar esta tarea hasta que un compañero me despertó de un codazo. Sin embargo, en ese momento me sobrevino la inspiración y pensé “¿Algo solidario?”, “¿Relacionado con .NET?”, “¡Caramba! ¿Y qué hay más solidario que una transacción?” Básicamente, una transacción en el contexto del desarrollo, significa un conjunto de operaciones que deben validarse todas como una sola. O todas o ninguna. Ya lo tenía.

    El concepto

    Desde los antiguos Sistemas Gestores de Bases de Datos (aka SGBD) que empezaron a soportar transacciones, hasta el nuevo espacio de nombres del NET Framework 2.0 “System.Transactions” han pasado muchos años, sin embargo cuando pensamos en una transacción siempre la asociamos a un medio capaz de persistir los datos, como una base de datos. Y es que la posibilidad de efectuar transacciones viene a solucionar el viejo problema que se produce cuando tenemos que asegurar que todo un conjunto de operaciones van a validarse o rechazarse de plano.

    Para comenzar una transacción hay que marcar el momento de inicio (Begin), a partir del cual todas las operaciones efectuadas no serán definitivas hasta que se decida terminar con ella ya sea aprobando (Commit) o rechazando la transacción (Rollback).

       1: BEGIN TRANSACTION
       2: UPDATE Orders SET Freight = Freight * 2 WHERE OrderDate = ‘20061225’
       3: UPDATE Customers SET Country = ‘France’ WHERE Country = ‘Francia’
       4: DELETE Customers WHERE Country = ‘Canada’
       5: COMMIT TRANSACTION
     

    Existen distintos tipos de transacciones, desde las transacciones implícitas más sencillas que son proporcionadas de forma transparente por la gran mayoría de SGBD, hasta transacciones más complejas que pueden llegar a estar distribuidas en varios recursos de una red.

    La concurrencia

    Hablar de transacciones es hablar de concurrencia, ya que hoy en día es casi imposible imaginar que seamos la única persona que accede a los datos en un momento dado. Como concurrencia se entiende la posibilidad de tener varios usuarios accediendo a la base de datos, consultando y escribiendo datos al mismo tiempo, manteniendo una vista consistente de los datos gracias al uso inteligente de bloqueos sobre filas, o de transacciones.

    Dicho de otro modo ¿Qué sucede si un usuario comienza una transacción, modifica un dato sin terminar la transacción, y a continuación otro usuario quiere recuperar el valor de este dato? ¿El segundo usuario podrá acceder al dato? En caso afirmativo ¿Qué valor recuperará, el antiguo o el nuevo?

    El aislamiento

    La respuesta a estas preguntas la tiene el llamado nivel de aislamiento. En función del nivel de aislamiento de la conexión los datos modificados podrán ser o no visibles para otros procesos, y podrán mostrar los cambios realizados sobre los datos o no. Por ejemplo si desde un proceso (A) iniciamos una transacción y modificamos el valor de una fila de una tabla:

       1: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
       2: BEGIN TRANSACTION
       3: UPDATE Orders SET Freight = 50 WHERE OrderID = 10248

    Desde un segundo proceso (B) no tendremos acceso a este registro hasta que el primer proceso (A) termine la transacción mediante COMMIT o ROLLBACK, o hasta que termine el tiempo de espera.

       1: SELECT Freight FROM Orders WHERE OrderID = 10248

    Sin embargo algunos SGBD como SQL Server admiten especificar sugerencias de bloqueo, que permitan suplantar el nivel de aislamiento y de este modo acceder al registro. En el siguiente ejemplo se utiliza NOLOCK como sugerencia de tabla para poder acceder a los datos modificados, que todavía no se han confirmado:

       1: SELECT Freight FROM Orders WITH (NOLOCK) WHERE OrderID = 10248

    Esto no siempre es lo mejor ya que en algunos casos como el anterior se podría dar lo que se llama una lectura no confirmada o dato sucio, ya que si la transacción del primer proceso termina siendo abortada, habremos obtenido como resultado un valor que jamás ha existido.

    El diseño

    Debemos ser conscientes de este comportamiento cuando diseñemos una aplicación, ya que nuestros objetos de negocio existirán dentro de este contexto transaccional, y una transacción habitualmente implicará a varios de estos objetos de negocio. Esto plantea el problema de diseño que un objeto de negocio no puede iniciar por si solo una transacción, ya que tal vez se encuentre implicado en una en este momento o esté a la espera de que finalice un proceso otro componente. El hecho es que como el objeto desconoce su contexto, no puede ser autosuficiente y de este modo no puede iniciar una transacción por sí solo.

     

    Contexto Transaccional

    Figura 1 - El contexto tradicional

    Contexto transaccional

    En estos casos debemos tener en cuenta que hay que crear el contexto transaccional “a mano”, es decir compartiendo la transacción entre todos los objetos implicados, por ejemplo, de modo que cada uno de ellos exponga una propiedad de tipo SqlTransaction o reciba un argumento con un objeto SqlTransaction en aquellos métodos que realizan acciones sobre los datos (una buena forma de realizar esto puede ser declarando una Interfaz ITransactionable y haciendo que todos nuestros objetos la implementen). De este modo la transacción se inicia antes de llamar a ningún objeto (por ejemplo en una clase manager encargada de dirigir la operación), y a continuación se realizan todas las llamadas a los objetos implicados informando el valor de la propiedad "Transaction” de cada uno de ellos, esto es propagando la transacción. Para terminar se confirma o se aborta la transacción desde el mismo objeto manager que la ha iniciado:

       1: //inicia la transacción
       2: SqlTransaction transaction = con.BeginTransaction();
       3: ...
       4: //crea e inserta en la BD n objetos "order"
       5: order o = new order(...)
       6: //propaga la transacción
       7: o.transaction = transaction;
       8: o.Save();
       9: ...
      10: //completa o aborta la transacción
      11: transaction.Commit();
    El nuevo vecino (System.Transactions)

    Una de las novedades más importantes a mi juicio del Framework 2.0 es la incorporación de un nuevo espacio de nombres que simplifica enormemente el diseño de las aplicaciones, ya que el nuevo objeto TransactionScope permite definir el contexto transaccional dentro del ámbito de su bloque Using, de modo que todo acceso a los datos que se realice dentro de ese ámbito se considera de forma implícita dentro de una transacción. Además, para aprobar la transacción basta con invocar el método “Complete” que proporciona, antes de llegar al final del bloque Using. En caso contrario, al llegar al final del bloque se invocará automáticamente al método “Dispose” anulando la transacción.

    Esto hace innecesario definir ningún contexto transaccional, de modo que no es necesario incluir más código en nuestras clases para crear explícitamente este contexto. Basta con usar el contexto transaccional implícito que proporciona un objeto TransactionScope cuando sea necesaria una transacción. Obviamente tampoco es necesario crear una conexión en el objeto manager, ya que cada objeto ahora si es autosuficiente y va a crear su propia conexión para guardar los datos, y ésta será asociada implícitamente al contexto transaccional.

       1: //inicia el contexto transaccional
       2: TransactionScope ts = new TransactionScope();
       3: using (ts)
       4: {
       5:     order o = new order(...)
       6:     o.Save();
       7:     //completa o aborta la transacción
       8:     ts.Complete();
       9: }
      10:  
     

    ¡Fantástico! Y todavía hay más: Por si fuera poco, además es capaz de promocionar de forma transparente una transacción ligera a una transacción distribuida.

    Transacciones distribuidas

    Cuando se crea una transacción, por defecto se intenta crear una transacción ligera que será controlada por el LTM (Lightweight Transaction Coordinator). Sin embargo en determinados escenarios no basta con eso y debemos recurrir al uso de transacciones distribuidas. Algunos escenarios que pueden activar la promoción de una transacción son:

    - Cuando se está usando un controlador de recursos que no implementa la interfaz IPromotableSinglePhaseNotification, como por ejemplo SQL Server 2000.

    - Cuando dos controladores de recursos se enlistan dentro de la misma transacción, como por ejemplo abrir dos conexiones distintas dentro del ámbito de la transacción.

    - Cuando la transacción interviene en varios dominios de aplicación.

    Las transacciones distribuidas son gestionadas mediante COM+ a través del DTC (Distributed Transaction Coordinator) y su uso hasta ahora no era una tarea trivial, ya que para crear nuestras clases debíamos heredar de la clase ServicedComponent como se muestra en el siguiente fragmento de código:

       1: using System.EnterpriseServices;
       2: [Transaction]
       3:  
       4: public class Class1:ServicedComponent
       5: {
       6:     [AutoComplete]
       7:     public void DoSomething
       8:     {
       9:         //Hacer algo...
      10:     }
      11: }
      12:  
     

    Ahora esta promoción también se produce ahora de forma totalmente transparente. En el tercer punto del proyecto de ejemplo que se puede encontrar en ElContextoTransaccional.zip (374 Kb) se muestra cómo podemos depurar el código de nuestra aplicación para ver el momento en el que se produce, incluso cómo mostrar el coordinador de transacciones distribuidas para verla en acción.

    DTC

    El coordinador de transacciones mostrando una transacción distribuida

    Creo que las ventajas del uso de transacciones distribuidas están bastante claras, sin embargo el hecho de que por debajo utilicen interoperabilidad con COM+ puede resultar un aspecto negativo. Bien, para aquellos que al igual que yo se han planteado esta cuestión, los animo a dar un vistazo al cuarto punto del proyecto de ejemplo, donde se compara el rendimiento en milisegundos creando 100 objetos en ambos tipos de contextos transaccionales.

Ofrecido por Community Server (Commercial Edition)