SqlConnection和避免升级到MSDTC

10

当我们需要在应用程序中进行数据库访问时,我们使用以下模式:

  • 对于查询,我们有一个静态工厂类,其中包含一个名为CreateOpenConnection的方法,仅执行new SqlConnection(myConnectionString)和调用Open()。在进行查询之前调用此方法,并在查询返回后处理连接。
  • 对于插入/更新/删除操作,我们使用一种工作单元模式,其中更改被批量提交并通过调用work.Commit()提交到数据库中,如下所示:

work.Commit:

using (var tranScope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
    using (var conn = DapperFactory.CreateOpenConnection())
    {
      var count = _changeTracker.CommitChanges(conn);

      tranScope.Complete();

      return count;
    }
}

在一般的Web服务中,这似乎效果很好,但当我试图与Rebus组合使用时,它目前给我带来了MSDTC麻烦。

据我所知,当Rebus处理队列中的消息时,它会创建一个新的TransactionScope,以防无法处理消息而进行回滚。现在,这本身是没有问题的。我可以在Rebus消息处理程序中打开一个新的SqlConnection而没有任何问题(但是,在同一个Rebus TransactionScope中同时使用我们的旧版Entity Framework查询和手动SqlConnection就不行,但我现在不认为这是问题)。但是昨天,我问了以下问题:

Rebus中某个消息类型的串行处理

答案似乎是使用Rebus的saga功能。我尝试实现这个并将其配置为使Rebus saga持续到一个新的SQL Server数据库(具有不同的连接字符串)。可能因为使用了该SQL Server持久性,而导致创建了一个SqlConnection,因为现在每次我尝试创建SqlConnection时,都会出现以下异常:

分布式事务管理器(MSDTC)的网络访问已被禁用。请使用Component Services管理工具为MSDTC在安全配置中启用网络访问的DTC。

为配置和性能开销,我非常非常希望避免启用MSDTC。虽然我可能错了,但它也似乎无必要。

我猜这里发生的事情是Rebus创建了一个环境的TransactionScope,并且它所创建的SqlConnection注册到了该scope。当我尝试创建自己的SqlConnection时,它也试图注册到该环境范围,并因涉及多个连接而被提升为MSDTC,导致失败。

我有一个解决方案,但不确定是否正确。我的想法是:

  • Enlist=false添加到应用程序的连接字符串中,以便它永远不会注册到环境事务中。
  • 修改Commit方法,使其不创建新的TransactionScope(因为我已经告诉它不应该再订阅),而是使用conn.BeginTransaction

就像这样:

var transaction = conn.BeginTransaction();

try
{
  var count = _changeTracker.CommitChanges(conn);
  transaction.Commit();
  return count;
}
catch
{
  transaction.Rollback();
  throw;
}
finally
{
  transaction.Dispose();
}

我不确定这是否是正确的方法,以及可能存在什么问题。

有什么建议吗?

更新:澄清一下,导致我问题的不是work.Commit(),我很确定它会工作,但我从来没有到达那里,因为我的查询失败了。

一个失败的示例:

public int? GetWarehouseID(int appID)
{
  var query = @"
select top 1 ID from OrganizationUnits o
where TypeID & 16 = 16 /* warehouse */";

  using (var conn = _dapper.OpenConnection())
  {
    var id = conn.Query<int?>(query).FirstOrDefault();

    return id;
  }
}

这个方法在Rebus创建TransactionScope后以及打开SqlConnection后被调用。在打开我的SqlConnection时,它尝试注册并崩溃。


如果您使用“Enlist=false”,那么这肯定会使您的TransactionScope无意义,因为连接不会在其中 - Marc Gravell
同样地,你的 BeginTransaction 代码也没有使用事务 - ADO.NET 事务需要在命令中显式指定,所以你肯定需要将 transaction 传递到 CommitChanges 中,对吧? - Marc Gravell
你能澄清一下你正在使用的 SQL Server 版本是哪个吗? - Marc Gravell
2个回答

3
我有些惊讶你会看到此信息,因为“RequiresNew”应该意味着它与其他事务隔离;通常来说,此消息意味着在事务范围内已经激活了2个连接——你确定没有其他代码在该块内创建/打开连接吗?
你提出的解决方案应该可行,虽然在某些情况下,“TransactionScopeOption.Suppress”可能比更改配置更方便(但两者都可以使用)。但是,问题在于ADO.NET事务必须传递给各个命令,所以您需要进行一些清理代码来完成此操作:
using(var transaction = conn.BeginTransaction()) {
    try {
        var count = _changeTracker.CommitChanges(conn, transaction);
        transaction.Commit();
        return count;
    } catch {
        transaction.Rollback();
        throw;
    }
}

在这里,CommitChanges 接受一个事务 - 可能使用可选参数:

int CommitChanges(DbConnection connection, DbTransaction transaction = null)
{ ... }

您对 DapperFactory 的命名表明您正在使用 "dapper" - 因此,无论它是否为 null,您都可以将其直接传递给 "dapper"。
conn.Execute(sql, args, transaction: transaction);

抱歉,为了澄清一下(请看我的更新),失败的不是“提交”而是查询。我的提交代码只是为了展示如果我添加“Enlist=false”并仍然保持“Commit”原子性的修改方式(因为它确实不再与我创建的“TransactionScope”有任何关系)。而传递事务的部分是我忽略的,谢谢你提醒我 :) - JulianR
@JulianR 我并没有认为 Commit 函数会出错;我试图阐述两个观点——首先,代码需要通过事务(这一点我忘记在示例中包含了,见编辑部分);其次,你的 try/catch/finally 过于复杂,可以简化。 - Marc Gravell
是的,我在提到您的评论,您期望RequiresNew起作用,它可能确实起作用。但是假设我进行所需的更改,传入DbConnection,您认为我的新方法会起作用吗?我担心的是插入/更新/删除不是原子操作,或者enlist=false对查询产生一些负面/奇怪的后果。在某种程度上,这感觉就像通过吞噬异常来“修复”异常,如果您知道我的意思 :) - JulianR
@JulianR 转换到完全不同的事务范式并不是那么容易接受的事情;但是,我同意你的观点,不理解为什么原始方法行不通确实让人不满意。这取决于你想花多长时间来尝试修复它,而不是转向更有用的东西。 - Marc Gravell

1

这在很大程度上取决于您使用的 SQL Server 版本。请参见这里以查看另一个 SO 问题,涉及类似问题的解决方案。

这与 SQL 2005 和 SQL 2008 在处理同一 TransactionScope 内的多个连接时的差异有关。即 SQL 2008 可以在同一 TransactionScope 中打开多个连接而不升级到 MSDTC。

这可能是您所看到的问题吗?

如果是这种情况,我认为唯一的两个选择是升级到 SQL 2008 或启用 MSDTC。我知道这两个选项都可能非常麻烦。


1
必须强调的是,2008年的处理仅适用于两个连接的连接字符串和线程标识相同,并且这些连接是串行打开的(而不是并发打开的)。 - Marc Gravell

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