Sobre las transacciones, las conexiones, el LTM y el DTC

Estos últimos días he estado investigando sobre el funcionamiento de System.Transactions y su relación con el LTM (Lightweight Transaction Manager) para las transacciones locales y el DTC para las transacciones distribuidas.

Uno de mis planteamientos iniciales fue comprobar la diferencia de rendimiento entre las transacciones escaladas al DTC y las transacciones gestionados sólo por el LTM. Rodrigo ya había hecho pruebas sobre el tema anteriormente, en las cuáles demostraba el coste en rendimiento que supone escalar al DTC y el problema de escabalidad que en principio parecía que esto suponía.

El objetivo de estas pruebas era valorar el problema que supone abrir dos conexiones a la misma base de datos SQL Server 2005 cuando estamos dentro de un TransactionScope. Al hacer esto, aunque el recurso sea el mismo, la transacción es escalada automáticamente al DTC. Esto sucede porque internamente si nosotros hacemos el .Close() de la conexión, cuando se vuelve a hacer un .Open() dentro de la transacción nos devuelve una conexión totalmente nueva, en vez de reaprovechar la conexión que habíamos cerrado y que está asociada a la transacción en curso.

Si queremos solucionar este problema, en principio la única solución sería abrir una conexión al iniciar la transacción y mantenerla hasta que esta se termina. Así evitamos escalar al DTC la transacción. De nuevo, solo si estamos usando SQL Server 2005 y solo vamos a trabajar dentro de la transacción que este recurso, si no la transacción siempre se escala al DTC. Mantener tanto tiempo abierta esta conexión no es mayor problema porque aunque no lo hiciesemos así, al hacer el .Close() de la conexión se devuelve al Pool de conexiones, pero como todavía está asociada a una transacción, el Pool no se la prestarás a nadie más hasta que la transacción termine.

Pero surge un problema. Si nuestros componentes de negocio se llaman entre si, y luego a direferentes DAOs en la capa de acceso a datos, tendremos que estar pansando la conexión ya abierta por todas estas clases. Eso hace que cuando vayamos a uno de nuestros componentes de negocio nos encontremos con referencias a System.Data.SqlClient por ejemplo!!!. Un poco feo.

Además, que pasa cuando una determinada operación no queremos que sea transaccional. Si seguimos el patrón anterior de pasar la conexión estaremos reteniendo la conexión entre los diferentes selects por ejemplo.

Cómo escala el DTC

He trabajo sobre las pruebas de Rodrigo, para verificar lo que comentaba sobre la escalabilidad del DTC y... he encontrado un bug en esas pruebas. Uno relacionado con la toma de tiempos que hacía que estos se fuesen sumando y sumando entre las diferentes iteraciones con lo cual producía los resultados que Rodrigo comentaba.

En mis pruebas he observado que escala de forma lineal en vez de exponencial aunque la diferencia en valor absoluto con respecto a hacer la misma transacción sin escalar al DTC es sustancial.

LTM vs DTC

Cómo no escalar al DTC, no retener la conexión para operaciones no transaccionales y no ensuciar nuestro código con acceso a datos fuera de la capa de acceso a datos

Lo que he hecho es crear un componente para gestionar las conexiones dentro de la aplicación. Ahora cada DAO deberá abrir y cerrar la conexión, pero en vez de hacerlo directamente con SqlConnection lo que hará será pedir una conexión a mi componente. Si el DAO está en una transacción, la conexión que se le devuelve se guarda en un diccionario asociada al ID de la transacción.

Cuando el DAO ya no necesita más la conexión, se la devuelve a mi componente, que si no se está en una transacción cierra la conexión y si no la deja por si algún otro DAO dentro de la transacción la va a necesitar.

Y ahí está la coña, que si estoy en una transacción, cuando otro DAO necesite una conexión, le devuelvo la que tengo en el diccionario para esa transacción, en vez de abrir una nueva que sería lo que sucedería que hago un .Open() de una SqlConnection. Así se evita escalar al DTC.

Finalmente, en algún momento se tiene que cerrar la conexión para una transacción y sacarla del diccionario. Suscribiendose al evento TransactionCompleted se puede conseguir, pues este se lanza independientemente que la transacción haya completado correctamente haciendo commit o se haya hecho un rollback. Eso si queremos podemos verlo en la propiedad Status de la clase TransactionInformation (algo tal que System.Transactions.Transaction.Current.TransactionInformation.Status)

Concluyendo

Dejo aquí el código del componente para que alguien le heche un ojo y le busque las pegas. Lo he hecho todo hoy rápido y a mata caballo. Es probable que necesite algunos cuantos ciclos de CPU más para confirmar que está todo bien.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Transactions;

namespace DTCTest.Solution
{
    
internal sealed class ConnectionPool
    {        
        
private static readonly ConnectionPool instance =
          
new ConnectionPool();

        
private Dictionary<stringSqlConnection> connections;
        
private string connectionString;
        
        
private ConnectionPool()
        {
            connections = 
new Dictionary<stringSqlConnection>();
            connectionString = System.Configuration.
ConfigurationManager.ConnectionStrings["DbConn"].ConnectionString;
        }

        
public static ConnectionPool GetConnectionPool()
        {
            
return instance;
        }

        
public SqlConnection Open()
        {
            
SqlConnection conn;
            
Transaction current = Transaction.Current;
            
if (current == null)
            {
                conn = 
new SqlConnection(connectionString);
                conn.Open();
                
return conn;
            }
            
else
            {
                
string localIdentifier = current.TransactionInformation.LocalIdentifier;
                
                
if (connections.TryGetValue(localIdentifier, out conn)) 
                {
                    
return conn;
                }
                
else 
                {
                    conn = 
new SqlConnection(connectionString);
                    conn.Open();                    
                    connections.Add(localIdentifier, conn);
                    current.TransactionCompleted += 
new TransactionCompletedEventHandler(current_TransactionCompleted);                    
                    
return conn;
                }                
            }
        }        

        
void current_TransactionCompleted(object sender, TransactionEventArgs e)
        {
            
Transaction current = e.Transaction;
            
            
string localIdentifier = current.TransactionInformation.LocalIdentifier;

            
SqlConnection conn;

            
if (connections.TryGetValue(localIdentifier, out conn))
            {
                conn.Close();
                connections.Remove(localIdentifier);
            }
        }

        
public void Close(SqlConnection conn) 
        {
            
Transaction current = Transaction.Current;
            
if (current == null)
                conn.Close();
        }
    }      
}
Comparte este post:

Comentarios

# Rodrigo Corral said:

Aunque Iván, supongo que por no urgar en la herida, no lo comenta, he de decir que yo en mi artículo sacaba la conclusión de que el DTC escalaba mál debido a un bug en mi código a la hora de medir tiempos.

Decir en mi descargo que ya comentaba en el post que los resultado me parecian sorprendentes y que pedia que alguien los revisase, como por exigencias del guión ha hecho Iván unos meses despues, desentrañando el misterio.

Además le ha dado una vuelta al tema y a logrado una excelente solución!!! Excelente artículo titán!!!!

Wednesday, September 27, 2006 6:14 PM
# La masa, el ladrillo, la bota, el bocadillo... said:

Sin duda la capacidad de usar &aacute;mbitos de transacci&oacute;n es un gran avance en lo que a legibilidad,

Wednesday, September 27, 2006 6:18 PM
# El Bruno said:

Muy buen post Ivan !!!

y que interesantes resultados, pero claro el DTC esta pensado para que el desarrollo de los componentes que trabajan con el mismo, sea independiente del proveedor y de la gestion de las conexiones. Aunque veo tu clase ConnectionPool mas que interesante.

Felicitaciones again :D

Saludos

Wednesday, September 27, 2006 6:53 PM
# Iván González said:

Bruno,

esto no limita en nada el uso del DTC, ni lo reemplaza. Si luego abres en uno de tus DAOs una conexión a Oracle por ejemplo, la transacción se escala al DTC y santas pascuas.

Se trata de evitar escalar al DTC en los casos que no es estrictamente necesario, ello es, mi BD es SQL Server 2005 y ni recurso (BD) es único para esa transacción.

Espero que sirva de aclaración.

Gracias por los comentarios!!!

Iván

Wednesday, September 27, 2006 7:14 PM
# El Bruno said:

Ivan,

es muy cierto lo que dices, y tb es cierto q solo se justificaría el DTC para entornos no SQL, o entornos mixtos (SQL / Oracle), o entornos con transacciones distribuidas, o ... joder son varios :D

Ahora lo unico que faltaria para completar la clase, es integrar (tal vez con EntLib) un sistema de trazas, para conocer el estado de las transacciones finalizadas y abortadas !!!

Tiembla el DTC !!!

Saludos

PD: Ahora q se m vino a la mente lo de las trazas, creo que ya tengo un piloto donde probarlo :D

Wednesday, September 27, 2006 8:31 PM
# Pablo Alvarez said:

Genial el articulo, Iván!

Precisamente hoy me encontré con un comportamiento no esperado respecto al DTC, que no se si viene muy al cuento pero como me resulto curioso, lo dejo caer por aqui.

Tengo un entorno con una pequeña aplicacion cliente de prueba que abre una conexion a una base de datos dentro de un TransactionScope. En la misma máquina, tengo un servidor SQL Server 2000 y un SQL Server 2005. Detengo el servicio de DTC en la maquina y lanzo la aplicación contra el servidor 2005, y compruebo que la conexion se realiza correctamente (usando LTM ya que DTC esta parado). Si realizo la misma prueba contra el SQL Server 2000, se queja de que no tengo el DTC arrancado y la transacción no llega a iniciarse.

Esto es así porque en 2005 podemos promover transacciones locales al DTC si fuera necesario, y sin embargo, en SQL Server 2000 no.

Imagino que esto será porque quizá se de el caso en el cual una transaccion comienza de modo local, y en un momento determinado otra transaccion decide que va a enlistar a la primera, promoviendo a ésta primera transaccion a una transaccion distribuida. El TDS que entiende el 2000 no tiene ninguna manera de notificar que una transaccion va a pasar al DTC, por lo que la transaccion completa fallaría. Para evitar esto, se habrá determinado que al crear una transaccion contra un SQL Server 2000, el resource manager que empleará será siempre el DTC, aunque inicialmente no se trate de una transaccion distribuida... por si las moscas ;)

Supongo que ya lo conoceriais, pero yo me topé hoy con esto por primera vez y como tiene cierta relación con el post, y no me apetecía dedicar a esto el primer post en mi blog, decidí postearlo en este cachito que Iván me presta amablemente ;)

Por cierto, soy nuevo por estos lares, asi que un saludo a todos!!! :)

Thursday, September 28, 2006 1:15 AM
# Iván González said:

Hola Pablo,

muy acertado tu comentario.

Thursday, September 28, 2006 3:18 PM
# David Salgado said:

msdtc-lovers!!!

Muy chulo el post Ivan, felicidades

ciao!

Monday, October 2, 2006 11:29 PM
# Cesar2 said:

Hace ya tiempo de esto y lo cierto es que me gustaria manejar las transacciones en la BLL sin usar SqlConnection en dicha capa. Alguien ha puesto esto en produccion? es thread-safe? alguna otra solucion? gracias

Tuesday, January 20, 2009 5:17 PM
# Hugo Silva said:

Tengo una duda, con una base de datos Oracle 10G Express Edition, cuando abro la conexión escala directamente al DTC.

Alguien sabe por que sucede esto ?

Gracias de antemano

Monday, February 9, 2009 3:30 AM
# maurimv said:

Pues de mi parte los créditos son para Rodrigo y para Iván por igual, dos grandes!

Saludos amigos

Friday, April 1, 2011 8:38 PM