¿En qué circunstancias una SqlConnection se enlista automáticamente en una TransactionScope Transaction ambiental?

201

¿Qué significa que una SqlConnection se "aliste" en una transacción? ¿Significa simplemente que los comandos que ejecuto en la conexión participarán en la transacción?

Si es así, ¿en qué circunstancias se inscribe automáticamente una conexión SqlConnection en una transacción TransactionScope ambiente?

Ver preguntas en los comentarios del código. Mi suposición a la respuesta de cada pregunta sigue a cada pregunta entre paréntesis.

Escenario 1: Abrir conexiones DENTRO de un alcance de transacción

using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = ConnectToDB())
{   
    // Q1: Is connection automatically enlisted in transaction? (Yes?)
    //
    // Q2: If I open (and run commands on) a second connection now,
    // with an identical connection string,
    // what, if any, is the relationship of this second connection to the first?
    //
    // Q3: Will this second connection's automatic enlistment
    // in the current transaction scope cause the transaction to be
    // escalated to a distributed transaction? (Yes?)
}

Escenario 2: Uso de conexiones DENTRO de un alcance de transacción que se abrió FUERA de él

//Assume no ambient transaction active now
SqlConnection new_or_existing_connection = ConnectToDB(); //or passed in as method parameter
using (TransactionScope scope = new TransactionScope())
{
    // Connection was opened before transaction scope was created
    // Q4: If I start executing commands on the connection now,
    // will it automatically become enlisted in the current transaction scope? (No?)
    //
    // Q5: If not enlisted, will commands I execute on the connection now
    // participate in the ambient transaction? (No?)
    //
    // Q6: If commands on this connection are
    // not participating in the current transaction, will they be committed
    // even if rollback the current transaction scope? (Yes?)
    //
    // If my thoughts are correct, all of the above is disturbing,
    // because it would look like I'm executing commands
    // in a transaction scope, when in fact I'm not at all, 
    // until I do the following...
    //
    // Now enlisting existing connection in current transaction
    conn.EnlistTransaction( Transaction.Current );
    //
    // Q7: Does the above method explicitly enlist the pre-existing connection
    // in the current ambient transaction, so that commands I
    // execute on the connection now participate in the
    // ambient transaction? (Yes?)
    //
    // Q8: If the existing connection was already enlisted in a transaction
    // when I called the above method, what would happen?  Might an error be thrown? (Probably?)
    //
    // Q9: If the existing connection was already enlisted in a transaction
    // and I did NOT call the above method to enlist it, would any commands
    // I execute on it participate in it's existing transaction rather than
    // the current transaction scope. (Yes?)
}
Triynko
fuente

Respuestas:

188

He hecho algunas pruebas desde que hice esta pregunta y encontré la mayoría, si no todas, las respuestas por mi cuenta, ya que nadie más respondió. Avísame si me he perdido algo.

Q1. Sí, a menos que se especifique "enlist = false" en la cadena de conexión. El grupo de conexiones encuentra una conexión utilizable. Una conexión utilizable es aquella que no está incluida en una transacción o que está incluida en la misma transacción.

Q2 La segunda conexión es una conexión independiente, que participa en la misma transacción. No estoy seguro de la interacción de los comandos en estas dos conexiones, ya que se ejecutan en la misma base de datos, pero creo que pueden ocurrir errores si se emiten comandos en ambos al mismo tiempo: errores como "Contexto de transacción en uso por otra sesión "

Q3. Sí, se escala a una transacción distribuida, por lo que al enlistar más de una conexión, incluso con la misma cadena de conexión, hace que se convierta en una transacción distribuida, que puede confirmarse comprobando un GUID no nulo en Transaction.Current.TransactionInformation .DistributedIdentifier. * Actualización: leí en alguna parte que esto se solucionó en SQL Server 2008, por lo que MSDTC no se usa cuando se usa la misma cadena de conexión para ambas conexiones (siempre que ambas conexiones no estén abiertas al mismo tiempo). Eso le permite abrir una conexión y cerrarla varias veces dentro de una transacción, lo que podría hacer un mejor uso del grupo de conexiones abriendo conexiones lo más tarde posible y cerrándolas lo antes posible.

Q4. No. Una conexión abierta cuando ningún alcance de transacción estaba activo, no se enlistará automáticamente en un alcance de transacción recién creado.

Q5. No. A menos que abra una conexión en el alcance de la transacción o alista una conexión existente en el alcance, básicamente NO HAY TRANSACCIÓN. Su conexión se debe alistar automática o manualmente en el alcance de la transacción para que sus comandos participen en la transacción.

Q6. Sí, los comandos en una conexión que no participa en una transacción se confirman tal como se emitieron, a pesar de que el código se ejecutó en un bloque de alcance de transacción que se retiró. Si la conexión no está enlistada en el alcance de la transacción actual, no está participando en la transacción, por lo que confirmar o revertir la transacción no tendrá efecto en los comandos emitidos en una conexión que no está en el alcance de la transacción ... como descubrió este tipo . Es muy difícil de detectar a menos que comprenda el proceso de alistamiento automático: solo ocurre cuando se abre una conexión dentro de un alcance de transacción activo.

Q7. Si. Una conexión existente se puede inscribir explícitamente en el alcance de la transacción actual llamando a EnlistTransaction (Transaction.Current). También puede alistar una conexión en un hilo separado en la transacción mediante el uso de una Dependencia de transacción, pero como antes, no estoy seguro de cómo pueden interactuar dos conexiones involucradas en la misma transacción contra la misma base de datos ... y pueden ocurrir errores, y por supuesto, la segunda conexión alistada hace que la transacción se convierta en una transacción distribuida.

Q8. Se puede lanzar un error. Si se utilizó TransactionScopeOption.Required, y la conexión ya estaba incluida en una transacción de alcance de transacción, entonces no hay error; de hecho, no hay una nueva transacción creada para el alcance, y el recuento de transacciones (@@ trancount) no aumenta. Sin embargo, si usa TransactionScopeOption.RequiresNew, recibirá un mensaje de error útil al intentar alistar la conexión en la nueva transacción de alcance de transacción: "La conexión actualmente tiene la transacción inscrita. Finalice la transacción actual y vuelva a intentarlo". Y sí, si completa la transacción en la que se inscribe la conexión, puede alistarla de forma segura en una nueva transacción. Actualización: si anteriormente llamó a BeginTransaction en la conexión, se produce un error ligeramente diferente cuando intenta alistarse en una nueva transacción de alcance de transacción: "No se puede alistar en la transacción porque hay una transacción local en progreso en la conexión. Finalice la transacción local y rever." Por otro lado, puede llamar de forma segura a BeginTransaction en SqlConnection mientras está enlistado en una transacción de alcance de transacción, y eso en realidad aumentará @@ trancount en uno, a diferencia de usar la opción Requerida de un alcance de transacción anidada, lo que no hace que incrementar. Curiosamente, si luego continúa creando otro ámbito de transacción anidado con la opción Requerido, no obtendrá un error,

Q9. Si. Los comandos participan en cualquier transacción en la que se enlista la conexión, independientemente de cuál sea el alcance de la transacción activa en el código C #.

Triynko
fuente
11
Después de escribir la respuesta a la P8, me doy cuenta de que estas cosas comienzan a parecer tan complicadas como las reglas de Magic: The Gathering. Excepto que esto es peor, porque la documentación de TransactionScope no explica nada de esto.
Triynko
Para Q3, ¿está abriendo dos conexiones al mismo tiempo usando la misma cadena de conexión? Si es así, será una transacción distribuida (incluso con SQL Server 2008)
Randy admite a Monica
2
No. Edito la publicación para aclarar. Tengo entendido que tener dos conexiones abiertas al mismo tiempo siempre causará una transacción distribuida, independientemente de la versión de SQL Server. Antes de SQL 2008, abrir solo una conexión a la vez, con la misma cadena de conexión, aún causaría un DT, pero con SQL 2008, abrir una conexión a la vez (nunca tener dos abiertas a la vez) con la misma cadena de conexión no causará un DT
Triynko
1
Para aclarar su respuesta para la Q2, los dos comandos deberían funcionar bien si se ejecutan secuencialmente en el mismo hilo.
Jared Moore
2
Sobre el tema de promoción del tercer trimestre para cadenas de conexión idénticas en SQL 2008, aquí está la cita de MSDN: msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx
pseudocoder
19

Buen trabajo Triynko, todas tus respuestas me parecen bastante precisas y completas. Algunas otras cosas que me gustaría señalar:

(1) Alistamiento manual

En su código anterior, usted (correctamente) muestra el alistamiento manual de esta manera:

using (SqlConnection conn = new SqlConnection(connStr))
{
    conn.Open();
    using (TransactionScope ts = new TransactionScope())
    {
        conn.EnlistTransaction(Transaction.Current);
    }
}

Sin embargo, también es posible hacerlo así, usando Enlist = false en la cadena de conexión.

string connStr = "...; Enlist = false";
using (TransactionScope ts = new TransactionScope())
{
    using (SqlConnection conn1 = new SqlConnection(connStr))
    {
        conn1.Open();
        conn1.EnlistTransaction(Transaction.Current);
    }

    using (SqlConnection conn2 = new SqlConnection(connStr))
    {
        conn2.Open();
        conn2.EnlistTransaction(Transaction.Current);
    }
}

Hay otra cosa a tener en cuenta aquí. Cuando se abre conn2, el código del grupo de conexiones no sabe que desea enlistarlo más tarde en la misma transacción que conn1, lo que significa que conn2 recibe una conexión interna diferente que conn1. Luego, cuando se enlista conn2, ahora hay 2 conexiones enlistadas, por lo que la transacción debe promoverse a MSDTC. Esta promoción solo se puede evitar utilizando el alistamiento automático.

(2) Antes de .Net 4.0, recomiendo configurar "Vinculación de transacción = desvinculación explícita" en la cadena de conexión . Este problema se soluciona en .Net 4.0, lo que hace que la desconexión explícita sea totalmente innecesaria.

(3) Rodar el tuyo CommittableTransactiony configurarlo Transaction.Currentes esencialmente lo mismo que lo que TransactionScopehace. Esto rara vez es realmente útil, solo para su información.

(4) Transaction.Current es hilo estático. Esto significa que Transaction.Currentsolo se establece en el hilo que creó el TransactionScope. Por lo tanto, varios subprocesos que ejecutan lo mismo TransactionScope(posiblemente usando Task) no es posible.

Jared Moore
fuente
Acabo de probar este escenario, y parece funcionar como lo describe. Además, incluso si utiliza el alistamiento automático, si llama a "SqlConnection.ClearAllPools ()" antes de abrir la segunda conexión, se escala a una transacción distribuida.
Triynko
Si esto es cierto, entonces solo puede haber una única conexión "real" involucrada en una transacción. La capacidad de abrir, cerrar y volver a abrir una conexión enlistada en una transacción TransactionScope sin escalar a una transacción distribuida es realmente una ilusión creada por el grupo de conexiones , que normalmente dejaría abierta la conexión dispuesta y devolvería esa misma conexión exacta si se re -Abierto para alistamiento automático.
Triynko
Entonces, lo que realmente está diciendo es que si deja de lado el proceso de alistamiento automático, cuando vaya a volver a abrir una nueva conexión dentro de una transacción de alcance de transacción (TST), en lugar de que el grupo de conexiones tome la conexión correcta (la que originalmente enlistado en el TST), apropiadamente toma una conexión completamente nueva, que cuando se enlista manualmente, hace que el TST se intensifique.
Triynko
De todos modos, eso es exactamente lo que estaba insinuando en mi respuesta a la Q1 cuando mencioné que está enlistado a menos que se especifique "Enlist = false" en la cadena de conexión, luego hablé sobre cómo el grupo encuentra una conexión adecuada.
Triynko
En lo que respecta a los subprocesos múltiples, si visita el enlace en mi respuesta a la P2, verá que mientras Transaction.Current es único para cada subproceso, puede adquirir fácilmente la referencia en un subproceso y pasarla a otro subproceso; sin embargo, acceder a un TST desde dos subprocesos diferentes da como resultado un error muy específico "Contexto de transacción en uso por otra sesión". Para realizar un subproceso múltiple en un TST, debe crear una DependantTransaction, pero en ese punto debe ser una transacción distribuida, porque necesita una segunda conexión independiente para ejecutar comandos simultáneos y MSDTC para coordinar los dos.
Triynko
1

Otra situación extraña que hemos visto es que si construyes una EntityConnectionStringBuilder, será un desastre TransactionScope.Currenty (creemos) se alistará en la transacción. Hemos observado esto en el depurador, donde TransactionScope.Current's current.TransactionInformation.internalTransactionespectáculos enlistmentCount == 1antes de construir, y enlistmentCount == 2después.

Para evitar esto, constrúyalo adentro

using (new TransactionScope(TransactionScopeOption.Suppress))

y posiblemente fuera del alcance de su operación (la estábamos construyendo cada vez que necesitábamos una conexión).

Todd
fuente