【问题标题】:MSDTC getting invoked. But why?MSDTC 被调用。但为什么?
【发布时间】:2015-07-24 16:52:56
【问题描述】:

对于我的数据访问,我在 API 级别使用 TransactionScopes 将整个操作包装在单个事务中,以便我的 SQL 操作可以在某种程度上是可组合的。我有一个托管 API 的 Web 项目和一个单独的服务库,该服务库是 SQL 的实现和调用。在操作(API 入口点)开始时,我打开 TransactionScope。每当在操作处理过程中需要 SqlConnection 时,请求 AmbientConnection 而不是直接建立新连接。 AmbientConnection 为当前事务查找或创建一个新的 SqlConnection。这样做应该允许良好的可组合性,但也避免调用 MSDTC,因为它应该为事务中的每个子操作继续使用相同的连接。事务完成后(使用scope.complete()),连接自动关闭。

问题是,每隔一段时间,MSDTC 仍会被调用,我不知道为什么。我以前成功地使用过这个,我相信我从来没有调用过 MSDTC。不过这次我觉得有两件事不同:1) 我使用的是 SQL Server 2008 R1 (10.50.4000)——不是我的选择——而且我知道 MSDTC 行为从这个版本开始发生了变化,也许不是所有的问题直到后来的版本才制定出来。 2) async-await 的使用是新的,我相信我必须使用TransactionScopeAsyncFlowOption.Enabled 来适应这个新功能,以防某些部分实现是异步的。或许需要采取更多措施。

我在连接字符串中尝试了Pooling=false,以防它被调用,因为两个独立的逻辑连接在单个池连接下错误地处理。但这没有用。

API 操作

// Exposed API composing multiple low-level operations within a single TransactionScope
// independent of any database platform specifics.
[HttpPost]
public async Task<IHttpActionResult> GetMeTheTwoThings()
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled))
    {
        var result = new TwoThings(
            await serviceLayer.GetThingOne(),
            await serviceLayer.GetThingTwo());

        scope.Complete();

        return Ok(result);
    }
}

服务层实现

public async Task<ThingOne> GetThingOne()
{
    using (var cmd = connManagement.AmbientConnection.CreateCommand())
    {
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.CommandText = "dbo.GetThingOne";

        return (ThingOne)(await cmd.ExecuteScalarAsync());
    }
}

public async Task<ThingTwo> GetThingTwo()
{
    using (var cmd = connManagement.AmbientConnection.CreateCommand())
    {
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.CommandText = "dbo.GetThingTwo";

        return (ThingTwo)(await cmd.ExecuteScalarAsync());
    }
}

AmbientConnection 实现

internal class SQLConnManagement
{
    readonly string connStr;
    readonly ConcurrentDictionary<Transaction, SqlConnection> txConnections = new ConcurrentDictionary<Transaction, SqlConnection>();

    private SqlConnection CreateConnection(Transaction tx)
    {
        var conn = new SqlConnection(this.connStr);

        // When the transaction completes, close the connection as well
        tx.TransactionCompleted += (s, e) =>
        {
            SqlConnection closing_conn;
            if (txConnections.TryRemove(e.Transaction, out closing_conn))
            {
                closing_conn.Dispose(); // closing_conn == conn
            }
        };

        conn.Open();

        return conn;
    }

    internal SqlConnection AmbientConnection
    {
        get
        {
            var txCurrent = Transaction.Current;

            if (txCurrent == null) throw new InvalidOperationException("An ambient transaction is required.");

            return txConnections.GetOrAdd(txCurrent, CreateConnection);
        }
    }

    public SQLConnManagement(string connStr)
    {
        this.connStr = connStr;
    }
}

不要使帖子过于复杂,但这可能是相关的,因为在我看来,每次调用 MSDTC 时,记录的堆栈跟踪都表明已经涉及下一个机制。我使用内置的 ObjetCache 缓存某些数据,因为它不会经常更改,所以我最多每分钟获取一次或其他什么。这有点花哨,但我不明白为什么 Lazy 生成器的处理方式与更典型的调用有任何不同,以及为什么这特别会导致 MSSDTC 有时被调用。我也尝试过LazyThreadSafetyMode.ExecutionAndPublication,以防万一,但这无论如何都无济于事(当然,异常只是在过期之前作为后续请求的缓存结果继续传递,这是不可取的)。

/// <summary>
/// Cache element that gets the item by key, or if it is missing, creates, caches, and returns the item
/// </summary>
static T CacheGetWithGenerate<T>(ObjectCache cache, string key, Func<T> generator, DateTimeOffset offset) where T : class
{
    var generatorWrapped = new Lazy<T>(generator, System.Threading.LazyThreadSafetyMode.PublicationOnly);

    return ((Lazy<T>)cache.AddOrGetExisting(
        key,
        generatorWrapped,
        offset))?.Value ?? generatorWrapped.Value;
}

public ThingTwo CachedThingTwo
{
    get
    {
        return CacheGetWithGenerate(
            MemoryCache.Default,
            "Services.ThingTwoData",
            () => GetThingTwo(), // ok, GetThingTwo isn't async this time, fudged example
            DateTime.Now.Add(TimeSpan.FromMinutes(1)));
    }
}

你知道为什么要调用 MSDTC 吗?

【问题讨论】:

  • 为了理智,您是使用多个数据库服务器还是以多种方式引用同一服务器,即(localhost - SERVERNAME - 192.168.2.120)在多个连接字符串中?此外,您是否可以引用同一集群上的两个节点(当您在集群上的节点之间移动时,您无法保证连接最终会在哪个节点上)。
  • 好主意,但它是非集群的单实例,只是连接字符串的一个版本。
  • @JasonKleban 你有没有想过这个问题?我发现使用 async/await 时事务升级了,我想知道它是否与此有关。
  • 我没有弄清楚为什么。我最终在数据访问操作体周围使用了TransactionScopeOption.RequiresNew,并且代码注释仍然想知道为什么没有它代码严格无法工作。

标签: c# sql sql-server-2008 async-await msdtc


【解决方案1】:

PublicationOnly 表示可以创建两个连接,丢弃一个。我很惊讶你犯了这个错误,因为你明确声明了PublicationOnly(而不是默认的安全模式,它是安全的)。您明确允许此错误。

由于某种原因,我没有看到您已经尝试过ExecutionAndPublication。由于不使用它是一个错误,请修复问题中的代码。

CreateConnection 也被破坏,因为在打开异常的情况下连接对象不会被释放。可能无害,但你永远不知道。

此外,请检查此代码是否存在 ASP.NET 请求超时时可能发生的线程中止。你在这里做的是非常危险和脆弱的事情。

我使用的模式是使用 IOC 容器来注入为整个请求共享的连接。该连接的第一个客户端打开它。请求结束事件将其关闭。简单,并且消除了所有令人讨厌的共享、可变、多线程状态。

为什么要对不想丢失的数据使用缓存?这可能是错误。不要那样做。

?.Value ?? generatorWrapped.Value 是关于什么的?字典永远不能返回 null。删除该代码。如果它可以返回 null 然后强制惰性值将创建第二个连接,所以这也是一个逻辑错误。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-11
  • 2011-08-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多