在单个事务中针对不同表进行多个并行(异步)SqlBulkCopy 插入的性能

Rei*_*l-- 3 c# sql-server sqlbulkcopy task-parallel-library async-await

长话短说

为什么async在单个事务上针对不相关的表并行运行多个 SqlBulkCopy 插入看起来就像串行运行一样?


语境

我有一些代码正在计算和存储大量数据。计算是预先完成的,因此代码的存储部分得到了要存储的一大堆数据。

我的数据库写入正在完成,SqlBulkCopy.WriteToServerAsync一般来说,它可以很好地完成工作。

我需要存储6个与业务相关的表,但与SQL无关。因此,我对它们的写入需要在一个事务中进行,以便任何一个写入上的错误都会恢复所有其他写入上的写入。

该代码的性能相当关键,因此我希望能够并行运行 BulkInsert。没有 FKey 或任何其他与之交互的表(数据完整性由代码管理),因此我看不出有任何理由认为这是不可能的。


我目前写的内容

我以为我知道如何编写所有代码并且能够使其全部正常工作,但是有一个我不明白的奇怪的性能下降:

很高兴提供您想要的实际代码位,但这已经是一个很长的 Q,并且代码会很长到 0。如果你确实想看什么,LMK。

我可以写:

  • “按顺序批量插入到每个表中,全部在单个事务中”。

    • 即我打开一个new SqlConnection()and .BeginTransaction(),
    • 然后我foreach翻了6张桌子,await InsertToTable(transaction)每张桌子都移动foreach到下一张桌子。
    • foreach结束时我.Commit()进行事务并关闭连接。
    • 我有一个大容量测试,该版本在184秒内运行(95%,+/- 2.45 秒)。
  • “按顺序批量插入每个表,每个表都有一个新的连接和事务。”

    • 即我foreach遍历了 6 个表,并且await InsertToTable()每个表在foreach移动到下一个表之前。
    • 在每次InsertToTable()调用中,我都会打开一个新的SqlConnectionand BeginTransaction,然后在从方法返回之前打开.Commit()一个 and 。.Close()
    • 我有一个大容量测试,该版本在185秒内运行(95%,+/- 3.34 秒)。
  • “并行批量插入每个表每个表都有一个新的连接和事务。”

    • thisTableTask = InsertToTable()即,我通过调用每个表并捕获Tasks 但尚未对 await它们进行 ing来启动所有 6 个任务。
    • await Task.WhenAll()捕获了 6 个任务。
    • 在每次InsertToTable()调用中,我都会打开一个新的SqlConnectionand BeginTransaction,然后在从方法返回之前打开.Commit()一个 and 。.Close()(但请注意,foreach 已移至下一个表,因为它不会await立即执行任务。
    • 我有一个大容量测试,该版本在144秒内运行(95%,+/- 5.20 秒)。
  • “并行批量插入每个表,全部在单个事务中”。

    • 即我打开一个new SqlConnection()and .BeginTransaction()
    • thisTableTask = InsertToTable(transaction)然后,我通过调用每个表并捕获Tasks 但尚未对 它们进行 ing来启动所有 6 个任务await
    • await Task.WhenAll()捕获了 6 个任务。
    • 一旦WhenAll结束,我就.Commit()进行事务并关闭连接。
    • 我有一个大容量测试,该版本在179秒内运行(95%,+/- 1.78 秒)。

在所有情况下,最终的 BulkInsert 如下所示:

using (var sqlBulk = BuildSqlBulkCopy(tableName, columnNames, transactionToUse))
{
    await sqlBulk.WriteToServerAsync(dataTable);
}

private SqlBulkCopy BuildSqlBulkCopy(string tableName, string[] columnNames, SqlTransaction transaction)
{
    var bulkCopy = new SqlBulkCopy(transaction.Connection, SqlBulkCopyOptions.Default, transaction)
    {
        BatchSize = 10000,
        DestinationTableName = tableName,
        BulkCopyTimeout = 3600
    };

    foreach (var columnName in columnNames)
    {
        // Relies on setting up the data table with column names matching the database columns.
        bulkCopy.ColumnMappings.Add(columnName, columnName);
    }

    return bulkCopy;
}

Run Code Online (Sandbox Code Playgroud)

当前性能统计

如上所列

  • 顺序 + 单次 Tran = 184s
  • 顺序 + 单独传输 = 185s
  • 并行+单独Tran = 144s
  • 并行+单Tran = 179s

前 3 个结果对我来说都很有意义。

#1 vs #2:只要插入全部有效,事务就不会做太多事情。数据库仍在相同的时间点执行所有相同的工作。

#2 vs #3:这就是并行运行插入的全部要点。通过并行运行插入,我们等待 SQL 完成任务的时间更少。我们让数据库并行执行大量工作,因此速度虽然没有提高 6 倍,但仍然足够了。


问题:

为什么最后一个案例这么慢?我可以修复它吗?

  • 并行+单Tran = 179

这几乎与串行执行一样慢,并且比并行执行慢了整整 25%,但有多个事务!

这是怎么回事?为什么async在单个事务上针对不相关的表并行运行多个 SqlBulkCopy 插入看起来就像串行运行一样?


非欺骗:

SqlBulkCopy 多个表在单个事务下插入或实体框架和经典 Ado.net 之间的批量插入操作(不并行运行查询)

在一个事务中对多个相关表使用 SqlBulkCopy(表是相关的,并且它们试图从中读回)

使用 SqlBulkCopy 和 Azure 进行并行批量插入(即并行加载到单个表中)

Dan*_*man 6

在同一 SQL Server 连接/事务上同时执行多个命令的唯一方法是使用多个活动结果集(MARS)。MARS 用于并行单事务案例,因为您对每个并行批量复制使用相同的连接/事务。

MARS 以交错而非并行方式执行 SELECT 和插入批量操作,因此您将获得与串行执行大致相同的性能。您需要具有不同连接的分布式事务,以便在同一事务范围内实现真正的并行执行。