【问题标题】:Hangfire Transactional Process (Unit of Work)Hangfire 事务处理(工作单元)
【发布时间】:2019-01-17 11:33:37
【问题描述】:

在下面的 .Net Framework 代码中,确保将 someEntity 对象插入到 db 中,然后执行 Publish 操作。但是,在 .Net Core 中我无法做到这一点。当我尝试运行这段代码时,会发生平台异常。

using (var transaction = new TransactionScope())
{
    SomeEntity someEntity = new SomeEntity();
    someEntity.Gui = Guid.NewGuid().ToString();

    _dataContext.SomeEntities.Add(someEntity);
    _dataContext.SaveChanges();

    _backgroundJobClient.Enqueue(() => PublishSomeEntityCreatedEvent(someEntity.Id)));

    transaction.Complete();
}

对于这种情况有什么好的解决方案吗?

注意:.Net Core 2.2 Console 应用,EntityFrameworkCore 2.1 和 Hangfire 1.6.21 用于测试


更新:整个堆栈跟踪

Hangfire.BackgroundJobClientException: Background job creation failed. See inner exception for details. ---> System.PlatformNotSupportedException: This platform
 does not support distributed transactions.
   at System.Transactions.Distributed.DistributedTransactionManager.GetDistributedTransactionFromTransmitterPropagationToken(Byte[] propagationToken)
   at System.Transactions.TransactionInterop.GetDistributedTransactionFromTransmitterPropagationToken(Byte[] propagationToken)
   at System.Transactions.TransactionStatePSPEOperation.PSPEPromote(InternalTransaction tx)
   at System.Transactions.TransactionStateDelegatedBase.EnterState(InternalTransaction tx)
   at System.Transactions.EnlistableStates.Promote(InternalTransaction tx)
   at System.Transactions.Transaction.Promote()
   at System.Transactions.TransactionInterop.ConvertToDistributedTransaction(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.SqlClient.SqlInternalConnectionTds.Activate(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionInternal.ActivateConnection(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionPool.PrepareConnection(DbConnection owningObject, DbConnectionInternal obj, Transaction transaction)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.Open()
   at Hangfire.SqlServer.SqlServerStorage.CreateAndOpenConnection()
   at Hangfire.SqlServer.SqlServerStorage.UseConnection[T](DbConnection dedicatedConnection, Func`2 func)
   at Hangfire.SqlServer.SqlServerConnection.CreateExpiredJob(Job job, IDictionary`2 parameters, DateTime createdAt, TimeSpan expireIn)
   at Hangfire.Client.CoreBackgroundJobFactory.Create(CreateContext context)
   at Hangfire.Client.BackgroundJobFactory.<>c__DisplayClass7_0.<CreateWithFilters>b__0()
   at Hangfire.Client.BackgroundJobFactory.InvokeClientFilter(IClientFilter filter, CreatingContext preContext, Func`1 continuation)
   at Hangfire.Client.BackgroundJobFactory.<>c__DisplayClass7_1.<CreateWithFilters>b__2()
   at Hangfire.Client.BackgroundJobFactory.CreateWithFilters(CreateContext context, IEnumerable`1 filters)
   at Hangfire.Client.BackgroundJobFactory.Create(CreateContext context)
   at Hangfire.BackgroundJobClient.Create(Job job, IState state)
   --- End of inner exception stack trace ---
   at Hangfire.BackgroundJobClient.Create(Job job, IState state)
   at Hangfire.BackgroundJobClientExtensions.Create(IBackgroundJobClient client, Expression`1 methodCall, IState state)
   at Hangfire.BackgroundJobClientExtensions.Enqueue(IBackgroundJobClient client, Expression`1 methodCall)
   at TopShelf_Hangfire_NetCore.BusinessService.Execute(DateTime utcNow) in C:\Projects\Practices\TopShelf_Hangfire_NetCore\BusinessService.cs:line 31
   at TopShelf_Hangfire_NetCore.StartupService._timer_Elapsed(Object sender, ElapsedEventArgs e) in C:\Projects\Practices\TopShelf_Hangfire_NetCore\StartupService.cs:line 35

【问题讨论】:

    标签: c# .net-core entity-framework-core transactionscope hangfire


    【解决方案1】:

    现在在使用 EntityFramework Core 3.0 或更高版本时可以使用。 这在 EFCore 2.x 中不起作用的原因是 EFCore 2.x 在不使用时没有关闭连接。相反,在释放上下文之前,DbConnection 一直保持打开状态。

    此行为在 EF Core 3.0 中已更改:https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#database-connection-is-now-closed-if-not-used-anymore-before-the-transactionscope-has-been-completed

    从 3.0 开始,EF Core 会在使用完连接后立即关闭它。 这支持您的方案,您希望在同一事务中加入 EfCore 和 Hangfire,而不升级到 MSDTC。

    【讨论】:

    • 这实际上是如何工作的?您没有将现有连接或 DbContext 传递给 Hangfire?你确定它标记在现有交易上吗?
    • 是的。 TransactionScope 是环境的。这意味着 Hangfire 将加入当前的环境事务范围。
    【解决方案2】:

    似乎正在启动分布式事务,.net 核心不支持 abd。

    由于您要访问多个资源管理器(您的数据库和hangfire 的数据库),因此事务范围会尝试升级要分发的事务。

    您可以将 hangfire 的 _backgroundJobClient.Enqueue() 带出范围,因此不会发生升级。

    您必须找到另一种方法来确保执行这两个操作(数据库更新、hangfire enqueue)

    编辑: 由于您无法进行交易,因此您必须设计服务以处理可能的故障情况。例如: 帐户服务将:

    1. 将创建的持久化到db

    2. 然后调用hangfire Enqueue

    3. 记录hangfire作业是在同一用户db中创建的事实。

    4. 用户服务必须轮询数据库,以查看是否创建了用户但未记录通知。

    其他微服务,应该能够处理重复通知。

    这样,如果创建了用户但没有发送通知,您的服务将重新发送(4)。

    如果失败发生在 (2) 和 (3) 之间,接收服务将忽略重复请求

    【讨论】:

    • 我注意到了。如果我从事务中取出hangfire 的入队过程,我无法确保这两个操作都会被执行。此外,我不需要交易。我真正的问题是“有没有已知的好的解决方案”。 ps:hangfire和ef连接同一个db
    • 对于 .net core 分布式事务是不可能的。即使您使用相同的数据库,它也无济于事,因为存在多个连接。回答你真正的问题:在我们的微服务时代,一致性是使用 Saga 模式和最终一致性等模式处理的。即只能在失败的情况下进行补偿,不能回滚。
    • 我知道这一点。在我的帐户微服务中,当新用户注册时,我尝试发布 AccountCreatedEvent。这些操作是事务性的。我的微服务仅对用户注册不负责。它还负责通知其他域ç。我必须确保消息发布操作和用户存储操作是事务性的。我试图弄清楚如何在没有分布式事务的情况下实现这一目标。
    • 我的愿望是在工作单元中使用 hangfire 和其他数据库操作。我提出这个问题的主要目的是在不使用事务范围的情况下了解任何可能的解决方案。如果数据库连接相同/共享,这会是一个可能的解决方案吗?感谢您的回复。我认为池技术是最后的资源解决方案。
    • 由于你不控制hangfire的数据库上下文,我看不出你如何共享连接。
    猜你喜欢
    • 1970-01-01
    • 2014-06-18
    • 1970-01-01
    • 1970-01-01
    • 2020-01-23
    • 1970-01-01
    • 1970-01-01
    • 2019-11-15
    • 1970-01-01
    相关资源
    最近更新 更多