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.

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<string, SqlConnection> connections;
private string connectionString;
private ConnectionPool()
{
connections = new Dictionary<string, SqlConnection>();
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();
}
}
}