“Transaction context in use by another session”的原因是什么?

15
我正在寻找有关此错误根源的描述:“Transaction context in use by another session”。有时我在单元测试中遇到它,因此无法提供复现代码。但我想知道这个错误的“设计”原因是什么。
更新:该错误从SQL Server 2008返回为SqlException错误。我遇到错误的地方似乎是单线程的。但可能由于同时运行多个测试(VS2008sp1中的MSTest),我会遇到单元测试交互而出现错误。但失败的测试看起来像:
  • 创建一个对象并将其保存在DB事务内(提交)
  • 创建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()

我找到了这些帖子:

但是我不明白 "多个线程在事务范围内共享同一个事务将导致以下异常:'Transaction context in use by another session.'" 是什么意思。所有单词我都懂,但是不理解它的意义。

实际上,我可以在线程之间共享系统事务。甚至还有专门的机制 - 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);
    }
}

在完成根TransactionScope的调用中失败。但错误不同:Unhandled Exception: System.Transactions.TransactionInDoubtException: 事务存在疑问。---> 已过期。在操作完成之前已超时或服务器没有响应。

总之,我想了解“事务上下文正在被另一个会话使用”的含义以及如何重现它。


一个小细节:你确定你使用了分布式事务吗? 你打开了两个连接:con1.Open(); con2.Open(); 但是连接字符串是相同的,而且你正在使用Sql2008。 据我所知,如果你使用Sql2008并且使用相同的连接字符串,事务不会升级为分布式事务。它仍然是“本地”的。以上仅供参考。 - Fabrizio Accatino
不,同时打开两个连接总是会导致分布式事务。无论它们是否具有相同的连接字符串。 - Shrike
@Fabrizio,如果相同的连接实例串行打开和关闭,则在 SQL2008 中我们会得到本地事务,但在 SQL2005 中得到分布式事务。 - Shrike
6个回答

7

这个回答晚了一点,但希望对其他人有用。回答包含三个部分:

  1. "Transaction context in use by another session."是什么意思
  2. 如何重现“Transaction context in use by another session.”错误

1. "Transaction context in use by another session."是什么意思

重要提示:在与 SQL Server 进行交互之前,SqlConnection 会获取事务上下文锁,并立即释放。

当您执行某些 SQL 查询时,SqlConnection 会查看是否有事务将其包装。它可以是 SqlTransactionSqlConnection 的本机事务),也可以是来自System.Transactions程序集的Transaction

当找到事务时,SqlConnection 使用它与 SQL Server 进行通信,此时它们之间的Transaction 上下文被独占锁定。

TransactionScope 是什么?它创建Transaction并向 .NET Framework 组件提供其信息,使每个人(包括 SqlConnection)都可以使用它(应该按设计来使用)。

因此,声明TransactionScope时,我们正在创建新的Transaction,该事务对于在当前Thread中实例化的所有可“交易”对象都可用。

描述的错误意味着以下内容:

  1. 我们在同一个TransactionContext下创建了多个SqlConnections(这意味着它们与同一事务相关)
  2. 我们要求这些SqlConnection同时与 SQL Server 进行通信
  3. 其中一个锁定了当前Transaction上下文,而另一个则引发了错误。

2. 如何重现“Transaction context in use by another session.”错误

首先,事务上下文是在执行 sql 命令时使用(“锁定”)的。因此,确切地重现这种行为是困难的。

但是,我们可以尝试通过在单个事务下启动运行相对较长的 SQL 操作的多个线程来实现。让我们在[tests]数据库中准备[dbo].[Persons]表:

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

基于Shrike代码示例,使用C#代码复现“事务上下文被另一个会话使用”的错误。

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/等请求并使用单线程工作者服务它们;
  • 在多线程应用程序中使用事务。始终如此。无论是读取还是写入;
  • 不要在多个线程之间共享单个事务。这会导致奇怪的、不明显的、超然的和无法重现的错误消息:
    • "Transaction context in use by another session.":在一个事务下与服务器进行多次交互;
    • "Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.":没有依赖关系的事务已经完成;
    • "The transaction is in doubt.";
    • ... 我想还有很多其他的...
  • 不要忘记为TransactionScope设置隔离级别。默认值为Serializable,但在大多数情况下,ReadCommitted足够了;
  • 不要忘记完成TransactionScopeDependentTransaction

2

在一个事务范围内共享相同的事务的多个线程会导致以下异常:'Transaction context in use by another session.'"

听起来很简单。如果您在同一事务中注册了两个不同的连接,然后尝试从不同的线程同时在这两个连接上发出命令,则可能会发生冲突。

换句话说,一个线程在一个连接上发出命令并在事务上持有某种锁定。另一个线程使用另一个连接,在同一时间尝试执行命令,但无法锁定正在被其他线程使用的相同事务上下文。


这个阶段很简单,但并不是重点。我展示了一个使用System.Transactions的多线程示例。并没有出现这样的错误(但有另一个错误)。 此外,请看一下SysTrans团队中某人的这篇帖子(我相信): http://www.pluralsight-training.net/community/blogs/jimjohn/archive/2005/05/01/7923.aspx - Shrike
如果SysTrans不支持多线程环境,为什么我们需要DependantTransaction类型呢? - Shrike

1

对于每个线程,您必须创建一个DependentTransaction,然后在线程内部使用ctor中的dependentTransaction创建并打开数据库连接。

            //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();
        }
    }

1

退一步,更专注于您的代码,少关注浮动的多个线程信息。

如果您的情况不涉及线程,则可能与未按您预期关闭的部分有关。

也许您调用的 SQL 代码没有达到提交事务指令。或者在那个级别上还涉及其他内容。也许您在 .net 代码中设置了 SqlConnection 实例以进行事务处理,并且正在重复使用同一实例,而该实例用于使用 TransactionScope 的其他代码。尝试在适当的位置添加 using() 指令,以确保所有内容都按您预期关闭。


0
在构建具有多个对象的Linq语句时,我如何处理这个问题是为每个类都创建一个构造函数,该构造函数接受数据上下文和相应的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;
        }

}

0
我有一个多线程应用程序,它执行一些数据操作并将结果存储在数据库中。由于不同的线程正在处理不同类型的数据,编写代码以收集结果并在一个线程中将其刷新到数据库中比让每个线程在完成时自己写出结果更加麻烦。
我想在事务中运行此操作,以便在任何一个子线程发生错误时可以选择撤消所有工作。添加事务开始引起了问题,这导致我发布了这篇文章,但我能够解决它们。单个事务中的多线程数据库访问是可能的。我甚至在同一个事务中同时使用LINQ-to-SQL和SqlBulkCopy。
我发现Ilya Chidyakin的答案非常有帮助。您需要向每个线程传递DependentTransaction,并使用它来创建新的TransactionScope。而且,您需要记住在每个线程中提交TransactionScope和DependentTransaction。最后,在所有子工作完成之前,必须等待提交“原始”事务。(实际上,DependentTransaction应该处理此问题,但在将事务添加到此项目之前,我已经使用Thread.Join等待所有工作完成。)
关键是,在任何给定时间只能有一个线程访问数据库。我只使用了一个信号量来阻止对数据库的访问,一次只允许一个线程访问。由于我的线程大部分时间都在计算,只有很少的时间写入数据库,所以我并没有因此遭受性能损失...然而,如果您的线程频繁使用数据库,则此要求可能会从多线程中删除性能优势,如果您希望将所有内容包含在一个事务中。
如果您有多个线程同时访问数据库,您将收到一个异常消息为“Transaction context in use by another session”的异常。如果您忘记提交每个线程中的所有事务,则在尝试提交最外层事务时,您将收到一个异常消息为“The transaction is in doubt”的异常。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接