【发布时间】:2017-06-09 12:38:42
【问题描述】:
在 Web 应用程序中,我们为应用程序中的各种数据库表提供分页搜索面板。我们目前允许用户选择单独的行,并通过 UI 在每个选定的实例中执行一些操作。
例如,文档记录面板提供了删除文档的功能。用户可以选中代表 15 个文档标识符的 15 个复选框,然后选择选项 > 删除。这工作得很好。
我希望为用户提供一个选项,以便对与查询匹配的所有行执行一些操作,以在面板中显示数据。
我们可能有 5,000 个符合某些搜索条件的文档,并希望允许用户删除所有 5,000 个。 (我知道这个例子有点做作;让我们忽略允许用户批量删除文档的“智慧”!)
为数千行执行一个方法是一个长时间运行的操作,所以我将把操作排队。认为这相当于 Gmail 将过滤器应用于所有符合某些搜索条件的电子邮件对话的能力。
我需要执行一个返回未知行数的查询,并为每一行插入一行到队列中(在下面的代码中,队列由ImportFileQueue 表示)。
我编码如下:
using (var reader = await source.InvokeDataReaderAsync(operation, parameters))
{
Parallel.ForEach<IDictionary<string, object>>(reader.Enumerate(), async properties =>
{
try
{
var instance = new ImportFileQueueObject(User)
{
// application tier calculation here; cannot do in SQL
};
await instance.SaveAsync();
}
catch (System.Exception ex)
{
// omitted for brevity
}
});
}
在使用事务包装调用的单元测试中运行此程序时,我收到 System.Data.SqlClient.SqlException: Transaction context in use by another session. 错误。
这很容易解决:
- 将数据库调用从异步更改为同步,或者
- 删除 Parallel.Foreach,并以串行方式遍历阅读器。
我选择了前者:
using (var reader = await source.InvokeDataReaderAsync(operation, parameters))
{
Parallel.ForEach<IDictionary<string, object>>(reader.Enumerate(), properties =>
{
try
{
var instance = new ImportFileQueueObject(User)
{
// Omitted for brevity
};
instance.Save();
}
catch (System.Exception ex)
{
// omitted for brevity
}
});
}
在典型的用例中,我的思考过程是:
- 外部阅读器通常会有数千行
- instance.Save() 调用是“轻量级”;在数据库中插入一行
两个问题:
- 有没有合理的方式在
Parallel.Foreach内部使用async/await,其中内部代码使用SqlConnection(避免TransactionContext错误) - 如果不是,考虑到我预期的典型用例,我选择利用 TPL 并放弃
async/await以实现合理的单行保存
What is the reason of “Transaction context in use by another session” 中建议的答案是:
尽可能避免多线程数据操作(无论 加载或保存)。例如。将 SELECT/UPDATE/ 等请求保存在 单个队列并使用单线程工作器为它们提供服务;
但我试图尽量减少总执行时间,并认为Parallel.Foreach 更有可能减少执行时间。
【问题讨论】:
-
你为什么要使用
Parallel?批量修改 N 行通常比尝试单独修改它们快 N 倍。如果您的更新速度很慢,请修复您的数据访问方法。例如,使用批处理来发送 一个 组命令,而不是多个单独的命令。 -
@EricPatrick 您的实际问题是什么?为什么要尝试并行插入记录?
-
@EricPatrick 插入时为什么要并发?插入大量数据的最快方法是使用批量加载,并将数据作为流发送到数据库,以网络和磁盘可以处理的速度最快。您也可以使用 SqlBulkCopy 在客户端执行此操作。 并发意味着锁定和争用,这就是为什么它会导致降低吞吐量。毕竟,您有一张网卡并写入同一个存储。同一网络、磁盘、CPU资源的并发连接内容
-
@EricPatrick 批量加载与发送一批命令不相同。数据库使用最少的日志记录,即它不会记录每一个 INSERT 命令。它将修改后的数据页复制到日志中,从而减少 IO 操作。
-
@EricPatrick 并行加载唯一有用的方法是,如果它不会导致争用,或者至少会导致最小的争用。在服务器端,这意味着写入不同的表,或同一张表的不同分区。每个操作只会锁定一个分区。
标签: c# sql-server task-parallel-library