В чем причина "контекста транзакции, используемого другим сеансом",

Я ищу описание корня этой ошибки: "Контекст транзакции используется другим сеансом".

Я иногда получаю это в одном из моих unittests, поэтому я не могу воспроизвести код. Но мне интересно, что такое "по дизайну" причина ошибки.

UPDATE: ошибка возвращается как SqlException из SQL Server 2008. Место, где я получаю ошибку, кажется однопоточным. Но, вероятно, у меня есть взаимодействие unittests, поскольку я получаю ошибку, когда запускают сразу несколько тестов (MSTest в VS2008sp1). Но неудачный тест выглядит следующим образом:

  • создать объект и сохранить его внутри транзакции DB (commit)
  • создать TransactionScope
  • пытается открыть соединение - здесь я получаю исключение SqlException с такой stacktrace:

.

System.Data.SqlClient.SqlException: Transaction context in use by another session.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at System.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransaction transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
   at System.Data.SqlClient.SqlInternalConnectionTds.PropagateTransactionCookie(Byte[] cookie)
   at System.Data.SqlClient.SqlInternalConnection.EnlistNonNull(Transaction tx)
   at System.Data.SqlClient.SqlInternalConnection.Enlist(Transaction tx)
   at System.Data.SqlClient.SqlInternalConnectionTds.Activate(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionInternal.ActivateConnection(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionPool.GetConnection(DbConnection owningObject)
   at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
   at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
   at System.Data.SqlClient.SqlConnection.Open()

Я нашел эти сообщения:

Но я не могу понять, что "Несколько потоков, разделяющих одну и ту же транзакцию в области транзакций, вызовут следующее исключение:" контекст транзакции, используемый другим сеансом ".). Все слова понятны, но не суть.

Фактически я могу совместно использовать системную транзакцию между потоками. И для этого существует даже специальный механизм - DependentTransaction class и метод Transaction.DependentClone.

Я пытаюсь воспроизвести usecase с первого поста:

  • Основной поток создает транзакцию DTC, получает DependentTransaction (созданный с помощью Transaction.Current.DependentClone в основном потоке
  • Детский поток 1 завершает транзакцию DTC путем создания области транзакции на основе зависимой транзакции (переданной через конструктор)
  • Детский поток 1 открывает соединение
  • Детский поток 2 завершает транзакцию DTC, создавая область транзакции на основе зависимой транзакции (переданной через конструктор)
  • Детский поток 2 открывает соединение

с таким кодом:

using System;
using System.Threading;
using System.Transactions;
using System.Data;
using System.Data.SqlClient;

public class Program
{
    private static string ConnectionString = "Initial Catalog=DB;Data Source=.;User ID=user;PWD=pwd;";

    public static void Main()
    {
        int MAX = 100;
        for(int i =0; i< MAX;i++)
        {
            using(var ctx = new TransactionScope())
            {
                var tx = Transaction.Current;
                // make the transaction distributed
                using (SqlConnection con1 = new SqlConnection(ConnectionString))
                using (SqlConnection con2 = new SqlConnection(ConnectionString))
                {
                    con1.Open();
                    con2.Open();
                }
                showSysTranStatus();

                DependentTransaction dtx = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
                Thread t1 = new Thread(o => workCallback(dtx));
                Thread t2 = new Thread(o => workCallback(dtx));
                t1.Start();
                t2.Start();
                t1.Join();
                t2.Join();

                ctx.Complete();
            }
            trace("root transaction completes");
        }
    }
    private static void workCallback(DependentTransaction dtx)
    {
        using(var txScope1 = new TransactionScope(dtx))
        {
            using (SqlConnection con2 = new SqlConnection(ConnectionString))
            {
                con2.Open();
                trace("connection opened");
                showDbTranStatus(con2);
            }
            txScope1.Complete();
        }   
        trace("dependant tran completes");
    }
    private static void trace(string msg)
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " : " + msg);
    }
    private static void showSysTranStatus()
    {
        string msg;
        if (Transaction.Current != null)
            msg = Transaction.Current.TransactionInformation.DistributedIdentifier.ToString();
        else
            msg = "no sys tran";
        trace( msg );
    }

    private static void showDbTranStatus(SqlConnection con)
    {
        var cmd = con.CreateCommand();
        cmd.CommandText = "SELECT 1";
        var c = cmd.ExecuteScalar();
        trace("@@TRANCOUNT = " + c);
    }
}

Не удается выполнить полный вызов root TransactionScope. Но ошибка отличается: Необработанное исключение: System.Transactions.TransactionInDoubtException: транзакция вызывает сомнения. --- > pired. Период ожидания истекает до завершения операции или сервер не отвечает.

Подводя итог: хочу понять, что означает "контекст транзакции, используемый другим сеансом", и как его воспроизводить.

Ответ 1

Это немного поздно для ответа:), но надеюсь, что это будет полезно для других. Ответ содержит три части:

  • Что означает "контекст транзакции, используемый другим сеансом" .
  • Как воспроизвести ошибку "Контекст транзакции, используемый другим сеансом".

1. Что означает "контекст транзакции, используемый другим сеансом" .

Важное замечание: Блокировка контекста транзакции выполняется непосредственно перед выпуском сразу после взаимодействия между SqlConnection и SQL Server.

Когда вы выполняете некоторый SQL-запрос, SqlConnection "смотрит", есть ли какая-либо транзакция, обертывающая его. Это может быть SqlTransaction ( "native" для SqlConnection) или Transaction из System.Transactions сборки.

Когда найденная транзакция SqlConnection использует ее для связи с SQL Server, и в настоящий момент они связывают Transaction контекст исключительно заблокирован.

Что делает TransactionScope? Он создает Transaction и предоставляет инфраструктуру компонентов .NET Framework об этом, поэтому каждый, включая SqlConnection, может (и по дизайну) использовать его.

Итак, объявляя TransactionScope, мы создаем новую транзакцию, доступную для всех "транзакционных" объектов, созданных в текущем Thread.

Описанная ошибка означает следующее:

  • Мы создали несколько SqlConnections под тем же TransactionContext (что означает, что они связаны с одной и той же транзакцией)
  • Мы попросили эти SqlConnection обмениваться данными с SQL Server одновременно
  • Один из них заблокировал текущий Transaction контекст и следующую заброшенную ошибку

2. Как воспроизвести ошибку "Контекст транзакции, используемый другим сеансом".

Прежде всего, контекст транзакции используется ( "заблокирован" ) сразу во время выполнения команды sql. Поэтому трудно воспроизвести такое поведение наверняка.

Но мы можем попытаться сделать это, запустив несколько потоков, выполняющих относительно длинные SQL-операции в рамках одной транзакции. Подготовьте таблицу [dbo].[Persons] в [tests] База данных:

USE [tests]
GO
DROP TABLE [dbo].[Persons]
GO
CREATE TABLE [dbo].[Persons](
    [Id] [bigint] IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [Name] [nvarchar](1024) NOT NULL,
    [Nick] [nvarchar](1024) NOT NULL,
    [Email] [nvarchar](1024) NOT NULL)
GO
DECLARE @Counter INT
SET @Counter = 500

WHILE (@Counter > 0) BEGIN
    INSERT [dbo].[Persons] ([Name], [Nick], [Email])
    VALUES ('Sheev Palpatine', 'DarthSidious', '[email protected]')
    SET @Counter = @Counter - 1
END
GO

И воспроизведите "контекст транзакции, используемый другим сеансом" . ошибка с кодом С# на основе примера кода Shrike

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

namespace SO.SQL.Transactions
{
    public static class TxContextInUseRepro
    {
        const int Iterations = 100;
        const int ThreadCount = 10;
        const int MaxThreadSleep = 50;
        const string ConnectionString = "Initial Catalog=tests;Data Source=.;" +
                                        "User ID=testUser;PWD=Qwerty12;";
        static readonly Random Rnd = new Random();
        public static void Main()
        {
            var txOptions = new TransactionOptions();
            txOptions.IsolationLevel = IsolationLevel.ReadCommitted;
            using (var ctx = new TransactionScope(
                TransactionScopeOption.Required, txOptions))
            {
                var current = Transaction.Current;
                DependentTransaction dtx = current.DependentClone(
                    DependentCloneOption.BlockCommitUntilComplete);               
                for (int i = 0; i < Iterations; i++)
                {
                    // make the transaction distributed
                    using (SqlConnection con1 = new SqlConnection(ConnectionString))
                    using (SqlConnection con2 = new SqlConnection(ConnectionString))
                    {
                        con1.Open();
                        con2.Open();
                    }

                    var threads = new List<Thread>();
                    for (int j = 0; j < ThreadCount; j++)
                    {
                        Thread t1 = new Thread(o => WorkCallback(dtx));
                        threads.Add(t1);
                        t1.Start();
                    }

                    for (int j = 0; j < ThreadCount; j++)
                        threads[j].Join();
                }
                dtx.Complete();
                ctx.Complete();
            }
        }

        private static void WorkCallback(DependentTransaction dtx)
        {
            using (var txScope1 = new TransactionScope(dtx))
            {
                using (SqlConnection con2 = new SqlConnection(ConnectionString))
                {
                    Thread.Sleep(Rnd.Next(MaxThreadSleep));
                    con2.Open();
                    using (var cmd = new SqlCommand("SELECT * FROM [dbo].[Persons]", con2))
                    using (cmd.ExecuteReader()) { } // simply recieve data
                }
                txScope1.Complete();
            }
        }
    }
}

И в заключение несколько слов об осуществлении поддержки транзакций в вашем приложении:

  • Избегайте многопоточных операций с данными, если это возможно (независимо от загрузки или сохранения). Например. сохранять SELECT/UPDATE/etc... запросы в одной очереди и обслуживать их с помощью однопоточного рабочего;
  • В многопоточных приложениях используются транзакции. Всегда. Везде. Даже для чтения;
  • Не разделяйте одну транзакцию между несколькими потоками. Это вызывает странные, неочевидные, трансцендентные и невоспроизводимые сообщения об ошибках:
    • "Контекст транзакции используется другим сеансом".: несколько одновременных взаимодействий с сервером в рамках одной транзакции;
    • "Истекло время ожидания. Период ожидания истекает до завершения операции или сервер не отвечает.": незатронутые транзакции были завершены;
    • "Сделка сомневается.";
    • ... и я принимаю много других...
  • Не забудьте установить уровень изоляции для TransactionScope. Значение по умолчанию Serializable, но в большинстве случаев ReadCommitted достаточно;
  • Не забудьте заполнить() TransactionScope и DependentTransaction

Ответ 2

"Несколько потоков, разделяющих один и тот же транзакция в области транзакций приведет к следующему исключению:" Контекст транзакции, используемый другим сессии ".

Звучит довольно просто. Если вы заручитесь двумя разными соединениями в одной транзакции, попробуйте одновременно выдавать команды для каждого из двух подключений из разных потоков, может возникнуть конфликт.

Другими словами, один поток выдает команду на одно соединение и удерживает какой-то замок в контексте транзакции. Другой поток, используя другое соединение, пытается выполнить команды в одно и то же время и не может блокировать тот же контекст транзакции, который используется другим потоком.

Ответ 3

Сделайте шаг назад и сосредоточьтесь больше на своем коде и меньше на информации о нескольких потоках.

Если ваш сценарий не связан с потоками, он может относиться к частям, которые не закрыты, как вы ожидаете.

Возможно, код sql, который вы вызываете, не достигает этой команды транзакции фиксации. Или на этом уровне есть что-то еще. Возможно, вы использовали экземпляр SqlConnection, устанавливающий транзакцию в .net-коде, и повторно используете тот же экземпляр для другого кода, который использует TransactionScope. Попытайтесь добавить с помощью() инструкции, где это необходимо, чтобы убедиться, что все закрыто, как вы ожидаете.

Ответ 4

Как я могу справиться с этой проблемой при создании операторов Linq с объектами mutlipe, должен быть конструктор для каждого класса, который принимает контекст данных и соответствующий метод GetDataContext() в каждом классе. при объединении классов я бы обновил экземпляры класса, проходящие в первом классе GetContext()

  public class CriterionRepository : ICriterionRepository
    {

        private Survey.Core.Repository.SqlDataContext _context = new Survey.Core.Repository.SqlDataContext();

        public CriterionRepository() { }

        public CriterionRepository(Survey.Core.Repository.SqlDataContext context)
        {            
            _context = context;
        }

...


        public Survey.Core.Repository.SqlDataContext GetDataContext()
        {
            return _context;
        }

}

Ответ 5

Вы должны создать DependentTransaction для каждого потока, а затем внутри потока создать и открыть db-соединение внутри TransacctionScope с помощью DependentTransaction в ctor.

            //client code / main thread
            using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, timeout))
            {
                Transaction currentTransaction = Transaction.Current;
                currentTransaction.TransactionCompleted += OnCompleted;
                DependentTransaction dependentTransaction;
                int nWorkers = Config.Instance.NumDBComponentWorkers;
                for (int i = 0; i < nWorkers; i++)
                {
                    dependentTransaction = currentTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
                    this.startWorker(dependentTransaction);
                }
                do
                {
                    //loop + wait
                    Thread.Sleep(150);
                } while (this.status == DBComponentStatus.Running);
                //No errors-commit transaction
                if (this.status == DBComponentStatus.Finished && this.onCanCommit())
                {
                    scope.Complete();
                }
            }

    //workers
    protected override void startWorker(DependentTransaction dependentTransaction)
    {
        Thread thread = new Thread(workerMethod);
        thread.Start(dependentTransaction);
    }

    protected override void workerMethod(object transaction)
    {
        int executedStatements = 0;
        DependentTransaction dependentTransaction;
        dependentTransaction = transaction as DependentTransaction;
        System.Diagnostics.Debug.Assert(dependentTransaction != null); //testing
        try
        {
            //Transaction.Current = dependentTransaction;
            using (TransactionScope scope = new TransactionScope(dependentTransaction))
            {
                using (SqlConnection conn = new SqlConnection(this.GetConnectionString(this.parameters)))
                {
                    /* Perform transactional work here */
                    conn.Open();
                    string statement = string.Empty;
                    using (SqlCommand cmd = conn.CreateCommand())
                    {

                    }
                }
                //No errors-commit transaction
                if (this.status == DBComponentStatus.Finished)
                {
                    scope.Complete();
                }
            }
        }
        catch (Exception e)
        {
            this.status = DBComponentStatus.Aborted;
        }
        finally
        {
            dependentTransaction.Complete();
            dependentTransaction.Dispose();
        }
    }

Ответ 6

У меня есть многопоточное приложение, которое выполняет некоторые манипуляции с данными и сохраняет результаты в базе данных. Поскольку разные потоки работают с различными типами данных, написание кода для сбора результатов и выведение его из базы в один поток является более громоздким, чем просто каждый поток записывает результаты сам по себе, когда это делается.

Я хотел запустить это в транзакции, так что у меня есть возможность вернуть всю работу в случае, если ошибка произошла в любом из дочерних потоков. Добавление транзакций вызвало проблемы, которые привели меня к этой публикации, но я смог проработать их. Возможно многопоточный доступ к базе данных в одной транзакции. Я даже использую LINQ-to-SQL и SqlBulkCopy вместе в одной транзакции.

Я нашел, что Илья Чидякин ответил очень полезно. Вам нужно передать DependentTransaction для каждого потока и использовать его для создания нового TransactionScope. И вам нужно помнить о том, чтобы зафиксировать транзакцию TransactionScope и DependentTransaction в каждом потоке. Наконец, вы должны ждать, чтобы выполнить свою "оригинальную" транзакцию, пока не будет выполнена вся работа с дочерью. (DependentTransaction должен позаботиться об этом, фактически, но я уже использовал Thread.Join, чтобы дождаться выполнения всей работы, прежде чем добавлять транзакции в этот проект.)

Ключевым моментом является то, что только один поток может обращаться к базе данных в любой момент времени. Я просто использовал семафор, чтобы блокировать доступ к базе данных по одному потоку за раз. Поскольку мои потоки тратят большую часть времени на вычисления и всего лишь немного времени на запись в базу данных, я действительно не взял на себя штраф за производительность из-за этого... Однако, если ваши потоки часто используют базу данных, это требование может существенно снижают эффективность работы многопоточности, если вы хотите, чтобы все содержалось в одной транзакции.

Если у вас есть несколько потоков, обращающихся к базе данных сразу, вы получите исключение с сообщением "Контекст транзакции, используемый другим сеансом". Если вы забудете совершить все транзакции в каждом потоке, вы получите сообщение об исключении с сообщением "Сделка сомневается", когда вы пытаетесь совершить транзакцию самого внешнего.