Что является причиной "контекста транзакции, используемого другим сеансом" - PullRequest
14 голосов
/ 18 мая 2010

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

Иногда я получаю его на одном из моих тестов юнитов, поэтому я не могу воспроизвести код поставщика Но мне интересно, что является «по замыслу» причиной ошибки.

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

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

.

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 и метод Transaction.DependentClone.

Я пытаюсь воспроизвести случай из первого поста:

  1. Основной поток создает транзакцию DTC, получает DependentTransaction (создается с использованием Transaction.Current.DependentClone в основном потоке
  2. Дочерний поток 1 включается в эту транзакцию DTC, создавая область транзакции на основе зависимой транзакции (передаваемой через конструктор)
  3. Дочерняя нить 1 открывает соединение
  4. Дочерний поток 2 включается в транзакцию DTC, создавая область транзакции на основе зависимой транзакции (передаваемой через конструктор)
  5. Дочерняя нить 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);
    }
}

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

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

Ответы [ 6 ]

5 голосов
/ 03 сентября 2016

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

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

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

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

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

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

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

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

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

  1. Мы создали несколько SqlConnections под одним и тем же TransactionContext (что означает, что они относятся к одной и той же транзакции)
  2. Мы попросили эти SqlConnection обмениваться данными с SQL Server одновременно
  3. Один из них заблокировал текущий 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', 'spalpatine@galaxyempire.gov')
    SET @Counter = @Counter - 1
END
GO

И воспроизвести «Контекст транзакции, используемый другим сеансом». ошибка с кодом C # на примере кода 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 голосов
/ 21 мая 2010

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

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

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

1 голос
/ 25 мая 2010

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

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

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

0 голосов
/ 01 августа 2017

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

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

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

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

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

0 голосов
/ 22 марта 2012

Вы должны создать DependentTransaction для каждого потока, затем внутри потока создать и открыть соединение БД внутри 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();
        }
    }
0 голосов
/ 24 августа 2010

Как мне решить эту проблему при построении операторов 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;
        }

}
...