我正在开发小型 POC 应用程序来测试 .NET7 对分布式事务的支持,因为这是我们工作流程中非常重要的方面。
到目前为止我一直无法让它工作,我不知道为什么。在我看来,要么是 .NET7 中存在某种错误,要么是我遗漏了一些东西。
简而言之,POC 非常简单,它运行 WorkerService,它做了两件事:
如果没有事务范围,这工作正常,但是,当添加事务范围时,我被要求使用以下方式打开对分布式事务的支持:
TransactionManager.ImplicitDistributedTransactions = true;
Worker服务中可执行代码如下:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
int number = 0;
try
{
while (!stoppingToken.IsCancellationRequested)
{
number = number + 1;
using var transactionScope = TransactionUtils.CreateTransactionScope();
await SaveDummyDataIntoTable2Dapper($"saved {number}").ConfigureAwait(false);
await messageSession.Publish(new MyMessage { Number = number }, stoppingToken)
.ConfigureAwait(false);
_logger.LogInformation("Publishing message {number}", number);
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
transactionScope.Complete();
_logger.LogInformation("Transaction complete");
await Task.Delay(1000, stoppingToken);
}
}
catch (Exception e)
{
_logger.LogError("Exception: {ex}", e);
throw;
}
}
交易范围是使用以下参数创建的:
public class TransactionUtils
{
public static TransactionScope CreateTransactionScope()
{
var transactionOptions = new TransactionOptions();
transactionOptions.IsolationLevel = IsolationLevel.ReadCommitted;
transactionOptions.Timeout = TransactionManager.MaximumTimeout;
return new TransactionScope(TransactionScopeOption.Required, transactionOptions,TransactionScopeAsyncFlowOption.Enabled);
}
}
保存到数据库的代码使用简单的简洁的 GenericRepository 库:
private async Task SaveDummyDataIntoTable2Dapper(string data)
{
using var scope = ServiceProvider.CreateScope();
var mainTableRepository =
scope.ServiceProvider
.GetRequiredService<MainTableRepository>();
await mainTableRepository.InsertAsync(new MainTable()
{
Data = data,
UpdatedDate = DateTime.Now
});
}
我必须在这里使用范围,因为存储库是有范围的,并且工作人员是单例的,所以它不能直接注入。
我尝试过使用 EF Core 进行持久化,也得到了相同的结果:
Transaction.Complete() 行通过,然后当尝试处理事务范围时它会挂起(有时它会设法插入几行然后挂起)。
没有事务范围,一切正常
我不确定我在这里遗漏了什么(如果有的话),或者只是这在 .NET7 中仍然不起作用?
请注意,我的机器上启用了 MSDTC,并且我在 Windows 10 上执行此操作
确保您使用的是 Microsoft.Data.SqlClient +v5.1 替换所有“usings”System.Data.SqlClient > Microsoft.Data.SqlClient
确保 ImplicitDistributedTransactions 设置为 True:
TransactionManager.ImplicitDistributedTransactions = true;
using (var ts = new TransactionScope(your options))
{
TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
... your code ..
ts.Complete();
}
我们已经能够使用以下代码解决这个问题。
通过此修改,DTC 实际上可以正确调用并在 .NET7 中工作。
using var transactionScope = TransactionUtils.CreateTransactionScope().EnsureDistributed();
扩展方法EnsureDistributed实现如下:
public static TransactionScope EnsureDistributed(this TransactionScope ts)
{
Transaction.Current?.EnlistDurable(DummyEnlistmentNotification.Id, new DummyEnlistmentNotification(),
EnlistmentOptions.None);
return ts;
}
internal class DummyEnlistmentNotification : IEnlistmentNotification
{
internal static readonly Guid Id = new("8d952615-7f67-4579-94fa-5c36f0c61478");
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Prepared();
}
public void Commit(Enlistment enlistment)
{
enlistment.Done();
}
public void Rollback(Enlistment enlistment)
{
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
enlistment.Done();
}
这是 10 年前的代码片段,但它可以工作(我猜测是因为 .NET Core 只是从 .NET 复制和重构了 DistributedTransactions 的代码,这也复制了错误)。
它的作用是立即创建分布式事务,而不是创建 LTM 事务,然后在需要时将其提升到 DTC。
更多详细说明可以在这里找到:
https://www.davidboike.dev/2010/04/forcously-creating-a-distributed-net-transaction/
对于使用可以分散在多个业务类中的多个嵌套
TransactionScope
实例的任何人,我想出了一个简单的类,它可以让您保持类似的方法,但启用 TransactionManager.ImplicitDistributedTransactions
标志并使用 lambda 将作用域链接到它功能方便使用。
*请注意,这仅经过了轻微测试,但似乎有效!我目前正在 EF Core 7.0.4 上运行。
public class InternalTransactionScope
{
/// <summary>
/// Executes an <see cref="Action"/> within the context
/// of a <see cref="TransactionScope"/> that has enabled
/// support for distributed transactions.
/// </summary>
/// <param name="action"></param>
public static void ExecuteTransaction(Action action)
{
//enable distributed transactions
TransactionManager.ImplicitDistributedTransactions = true;
using (var scope = new TransactionScope())
{
//link this scope to our overall transaction
TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
//execute and complete the scope
action();
scope.Complete();
}
}
/// <summary>
/// Executes an <see cref="Action"/> within the context
/// of a <see cref="TransactionScope"/> that has enabled
/// support for distributed transactions and returns the
/// result of the execution.
/// </summary>
/// <typeparam name="T">The type of return value expected.</typeparam>
/// <param name="action">The action to execute and retrieve a value from.</param>
/// <returns>An instance of <typeparamref name="T"/> representing the result of the request.</returns>
public static T ExecuteTransaction<T>(Func<T> action)
{
//enable distributed transactions
TransactionManager.ImplicitDistributedTransactions = true;
using (var scope = new TransactionScope())
{
//link this scope to our overall transaction
TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
//execute and complete the scope
var result = action();
scope.Complete();
return result;
}
}
}
在撰写本文时,事务范围曾经在 .NET Framework 上运行时存在死锁问题。死锁已作为 https://github.com/dotnet/SqlClient/pull/1242 的一部分得到修复,但从未移植到 .NET。随着 Windows 上引入对 .NET 7 及更高版本的分布式事务支持,可能会遇到同样的死锁。我已经提交了一个 pull request 希望能解决这个问题。