TransactionScope()和并行查询执行

3

我们尝试在事务范围内运行并行查询,以提高代码的性能。我们需要对数据库进行多个无关的更改。我们可以像这样运行代码:

using(var tran = new System.Transactions.TransactionScope())
{
    await queryMethod1Async();
    await queryMethod2Async();
    await queryMethod3Async();

    tran.Complete();
}

然而,由于这些方法相互独立,我们希望像这样运行代码:

using(var tran = new System.Transactions.TransactionScope())
{
    var tasks = new List<Task>();

    tasks.Add(queryMethod1Async());
    tasks.Add(queryMethod2Async());
    tasks.Add(queryMethod3Async());

    await Task.WhenAll(tasks);

    tran.Complete();
}

我们在并行执行方面遇到了一些问题:
  • 同时运行查询似乎会引发事务升级。在升级期间,有时会发生错误:
The wait operation timed out  --> There is already an open DataReader associated with this Command which must be closed first.    
at System.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransactionYukon(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)     
at System.Data.SqlClient.SqlDelegatedTransaction.Promote() --> Failure while attempting to promote transaction.    
at System.Data.SqlClient.SqlDelegatedTransaction.Promote()     
at System.Transactions.TransactionStatePSPEOperation.PSPEPromote(InternalTransaction tx)     
at System.Transactions.TransactionStateDelegatedBase.EnterState(InternalTransaction tx) --> The transaction has aborted.    
at System.Transactions.TransactionStateAborted.CheckForFinishedTransaction(InternalTransaction tx)     
at System.Transactions.Transaction.Promote()     
at System.Transactions.TransactionInterop.ConvertToOletxTransaction(Transaction transaction)    
at System.Transactions.TransactionInterop.GetExportCookie(Transaction transaction, Byte[] whereabouts)     
at System.Data.SqlClient.SqlInternalConnection.GetTransactionCookie(Transaction transaction, Byte[] whereAbouts)     
at System.Data.SqlClient.SqlInternalConnection.EnlistNonNull(Transaction tx)     
at System.Data.SqlClient.SqlInternalConnection.Enlist(Transaction tx)     
at System.Data.ProviderBase.DbConnectionInternal.ActivateConnection(Transaction transaction)   

经过一些调查,看起来是因为在升级过程中使用了原始事务连接,但在并行执行期间,这个连接可能正在被使用。我们尝试启用MARS以避免此问题,但这会导致不同的错误:
Current Microsoft Distributed Transaction Coordinator (MS DTC) transaction must be committed by remote client.    
at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)     
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)     
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)     
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.ExecuteTransactionYukon(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)     
at System.Data.SqlClient.SqlDelegatedTransaction.SinglePhaseCommit(SinglePhaseEnlistment enlistment) --> The transaction has aborted.    

为了澄清,上述每种方法都创建了一个新的sqlconnection,并且我们已经正确配置了MSDTC。
我不确定为什么会出现第二个错误,但我有一种感觉我走错了方向。在事务范围内执行并行查询是否可行?如果可以,应该采取哪种正确的方法?

恕我直言,正确的措辞应该是“并发查询执行”,因为TransactionScope()上下文管理器的范围最终只注入了一个“宏”结构屏障,使得所有异步“仅”[CONCURRENT]任务都要等待最慢的任务完成后,代码执行才能离开该范围。这种手动同步屏障对于独立的、否则不协调的任务的基本异步“仅”[CONCURRENT]操作不是上下文的属性,也不是真正的-[PARALLEL]问题,只是一种“尽快退出屏障”,强制等待所有“仅”[CONCURRENT]流程完成。 - user3666197
4个回答

0

你可以尝试几个选项:

  1. 启用 MARS,使用 TransactionScopeAsyncFlowOption.Enabled 参数创建你的事务范围:
using(var tran = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)
{...

我使用了 System.Data.SqlClient.SqlConnection 和 dapper 来读取/写入数据,效果很好。

  1. 如果你正在使用 Entity Framework Core,你可以使用 SemaphoreSlim 来同步访问你的 DbContext,以避免这个问题。示例:
// declare semaphore
var syncSemaphore = new SemaphoreSlim(1);
// [...]
// and use it in all your async functions like this:
async Task queryMethod1Async()
{
    await syncSemaphore.WaitAsync();
    try
    {
       // Use your database connection - read or write and release semaphore so other tasks can access
    }
    finally
    {
        syncSemaphore.Release();
    }            
}

我们已经启用了asyncflowoption,并且我们所有使用实体框架的查询都使用自己的dbcontext实例。但在这个例子中,事务中的任何查询都没有使用实体框架。 - PaulVrugt

0
在事务范围内执行并行查询是否可行?如果可以,正确的方法是什么?
是的。您必须为每个查询使用单独的SqlConnection,并且必须正确配置和运行MSDTC。但是这通常没有什么用处,因为每个命令都可以在SQL Server上并行运行,因此同时运行多个命令通常不会减少总运行时间。

我们已经为每个查询创建了单独的SqlConnection,并且MSDTC已正确配置,但仍然出现上述错误。我也不同意这种说法很少有用。我知道每个命令可以在SQL服务器上并行运行,但是当下一个命令被发送到SQL服务器时,只有在前一个命令完成后,才会运行任何内容。 - PaulVrugt
这个应该可以工作,但是MSTDC很难进行故障排除。你可以考虑使用3个单独的事务,在它们全部完成后提交它们。 - David Browne - Microsoft
1
拥有3个独立的事务会使使用事务的目的失去意义。我需要所有操作都完成,或者全部不完成。但是我可以向您保证,我们已经正确配置了MSDTC。我们正在为数百个客户运行企业SaaS环境,并且已经使用msdtc十年了。 - PaulVrugt
分离的事务思想是,当你试图提交Sql Server事务时,它很少会失败(除非你正在使用内存中的OLTP)。任何故障都应该发生在任何尝试提交之前,并且及时回滚所有事务。因此,如果MTDTC正常工作,您可以将代码简化为一个可重现的示例并将其添加到您的问题中吗? - David Browne - Microsoft
我现在理解了你的想法,但是我的理解是否正确是你的结论是在单个事务中执行并行查询不应该被做?如果这不被“支持”,我会感到非常惊讶,我需要完全改变我的代码设置来运行并行事务以实现这一点。这将使代码变得更加复杂和难以理解/维护。 - PaulVrugt

0

我认为最好在你的数据库中创建一个过程来运行所有查询。 然后你可以通过数据集在一个请求中获取它。 现在在数据集中使用每个数据表。


0

你的任何一个queryMethodXAsync是否会生成自己的事务(作为嵌套事务),并且在尝试完成TransactionScope之前没有回滚或提交它?

如果你调用使用事务但没有以正确的方式处理事务的存储过程,就可能发生这样的情况。

你的异常中是否有更多详细信息的InnerExcpetion?


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