【问题标题】:TransactionScope enlistment with async/await使用异步/等待的 TransactionScope 登记
【发布时间】:2017-02-07 22:01:44
【问题描述】:

原始问题

对于针对数据库的集成测试,我一直在 NUnit SetUp 方法中设置 TransactionScope 并在 TearDown 中回滚。当我将测试切换为对所有内容使用异步时,更改并没有回滚。我将SetUpasync Task 切换到void,它开始按预期运行。

源自案例的简短问题

将 TransactionScope 与 async/await 一起使用时,您是否需要在与 TransactionScope 相同的线程上创建 SqlConnection 才能传播到所有后续异步操作?

长问题

.NET 将 TransactionScopeAsyncFlowOption 添加到 TransactionScope 中,并将其描述为控制“与事务范围关联的环境事务是否会跨线程继续流动”

根据我看到的行为,您似乎仍然需要在 TransactionScope 的根线程上实例化您的 SqlConnections,否则命令不会自动加入环境事务中。这具有机械意义,我只是在文档中的任何地方都找不到它。所以我想我想知道是否有人对此主题了解更多?

这是测试用例(使用 NUnit 和 Dapper),当我试图弄清楚我的具体问题发生了什么时,这是一个超时,因为表被事务锁定,我的第二个连接未登记在(我认为?)

与 NUnit 相关的附注:如果您正在测试异步代码并且想要在 TransactionScope 中运行所有内容,请不要让您的 [SetUp] 方法异步任务。如果这样做,它可能会在与您的实际测试方法不同的线程上运行,并且您的连接不会被纳入事务中。

public class SqlConnectionTimeout
{
    public string DatabaseName = "AsyncDeadlock_TestCase";
    public string ConnectionString = "";

    [Test, Explicit]
    public void _RecreateDatabase()
    {
        using (var connection = IntegrationTestDatabase.RecreateDatabase(DatabaseName))
        {
            connection.Execute(@"
                CREATE TABLE [dbo].[example](
                    [id] [int] IDENTITY(1,1) NOT NULL,
                    [number] [int] NOT NULL,
                    CONSTRAINT [PK_example] PRIMARY KEY CLUSTERED 
                    (
                        [id] ASC
                    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
                ) ON [PRIMARY];

                CREATE TABLE [dbo].[exampleTwo](
                    [id] [int] IDENTITY(1,1) NOT NULL,
                    [number] [int] NOT NULL,
                    CONSTRAINT [PK_exampleTwo] PRIMARY KEY CLUSTERED 
                    (
                        [id] ASC
                    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
                ) ON [PRIMARY];
            ");
        }
    }

    [Test]
    public async Task Timeout()
    {
        TransactionScope transaction = null;
        SqlConnection firstConnection = null;

        Task.Factory.StartNew(() =>
        {
            transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

            firstConnection = new SqlConnection(ConnectionString);
            firstConnection.Open();
        }).Wait();

        using (transaction)
        {
            using (firstConnection)
            {
                using (var secondConnection = new SqlConnection(ConnectionString))
                {
                    await secondConnection.OpenAsync();

                    await firstConnection.ExecuteAsync("INSERT INTO example (number) VALUES (100);");

                    Assert.ThrowsAsync<SqlException>(async () => await secondConnection.QueryAsync<int>(
                        new CommandDefinition("SELECT * FROM example", commandTimeout: 1)
                    ));
                }
            }
        }
    }

    [Test]
    public async Task NoTimeout()
    {
        TransactionScope transaction = null;
        SqlConnection firstConnection = null;
        SqlConnection secondConnection = null;

        Task.Factory.StartNew(() =>
        {
            transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

            firstConnection = new SqlConnection(ConnectionString);
            firstConnection.Open();

            secondConnection = new SqlConnection(ConnectionString);
            secondConnection.Open();
        }).Wait();

        using (transaction)
        {
            using (firstConnection)
            {
                using (secondConnection )
                {
                    await firstConnection.ExecuteAsync("INSERT INTO example (number) VALUES (100);");

                    await secondConnection.QueryAsync<int>(
                        new CommandDefinition("SELECT * FROM example", commandTimeout: 1)
                    );
                }
            }
        }

        // verify that my connections correctly enlisted in the transaction
        // and rolled back my insert

        using (var thirdConnection = new SqlConnection(ConnectionString))
        {
            thirdConnection.Open();

            var count = await thirdConnection.ExecuteScalarAsync("SELECT COUNT(*) FROM example");
            Assert.AreEqual(0, count);
        }
    }
}

【问题讨论】:

  • 你能明确地将命令与适当的事务关联起来吗?
  • 请记下您正在使用的 .NET 运行时。
  • TransactionScope 不影响异步操作。该选项是必需的,因为await 没有将TransactionScope 保留到以前版本的同步上下文中。 测试 代码虽然使用Task.StartNew 在不同的线程上打开连接。没有要保留的上下文。 为什么你这样做而不是使用SqlConnection.OpenAsync()
  • 换句话说 - 您的代码明确丢弃 TransactionScope。没有可登记的环境 TransactionScope。
  • “与事务范围关联的环境事务是否会跨线程延续” - 是的,但是您的代码没有使用线程延续来链接创建 TransactionScope 和更高版本的块using 块 - 所以人们不会期望这个选项在这里有任何影响。换句话说,您在 StartNew lambda 中创建了一个环境事务,但在周围的代码中从来没有环境事务。

标签: c# .net sql-server async-await


【解决方案1】:

基于我的回答中的 cmets,特别是 Panagiotis Kanavos 的回答:

transactionscope 的 async 选项导致 TransactionScope 通过同步上下文从一个任务传递到另一个任务。我的问题中的测试代码在与使用 Task.StartNew 打开 TransactionScope 不同的线程上打开了连接,因此没有要传递的上下文,也没有要传播的事务。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 2016-07-07
    • 2016-03-25
    • 2017-10-09
    相关资源
    最近更新 更多