我正在尝试提高复杂数据库读取操作的性能。我发现一些代码,在有限的测试中,执行速度比以前使用各种技术(包括手动调整的存储过程)的尝试快得多。它使用 Dapper,但 Dapper 并不是主要的关注点。
public IEnumerable<Order> GetOpenOrders(Guid vendorId)
{
var tasks = GetAllOrders(vendorId)
.Where(order => !order.IsCancelled)
.Select(async order => await GetLineItems(order))
.Select(async order =>
{
var result = (await order);
return result.GetBalance() > 0M ? result : null;
})
.Select(async order => await PopulateName(await order))
.Select(async order => await PopulateAddress(await order))
.ToList();
Task.WaitAll(tasks.ToArray<Task>());
return tasks.Select(t => t.Result);
}
private IDbConnection CreateConnection()
{
return new SqlConnection("...");
}
private IEnumerable<Order> GetAllOrders(Guid vendorId)
{
using (var db = CreateConnection())
{
return db.Query<Order>("...");
}
}
private async Task<Order> GetLineItems(Order order)
{
using (var db = CreateConnection())
{
var lineItems = await db.QueryAsync<LineItem>("...");
order.LineItems = await Task.WhenAll(lineItems.Select(async li => await GetPayments(li)));
return order;
}
}
private async Task<LineItem> GetPayments(LineItem lineItem)
{
using (var db = CreateConnection())
{
lineItem.Payments = await db.QueryAsync<Payment>("...");
return lineItem;
}
}
private async Task<Order> PopulateName(Order order)
{
using (var db = CreateConnection())
{
order.Name = (await db.QueryAsync<string>("...")).FirstOrDefault();
return order;
}
}
private async Task<Order> PopulateAddress(Order order)
{
using (var db = CreateConnection())
{
order.Address = (await db.QueryAsync<string>("...")).FirstOrDefault();
return order;
}
}
这有些简化,但我希望它能突出我的主要问题:
- 这段代码是个好主意吗?
我知道可以通过重复使用同一个连接来提高安全性,但在我的测试中创建多个连接会使速度提高一个数量级。我还测试/计算了数据库本身的并发连接数,我看到有数百条语句同时运行。
一些相关问题:
- 我应该使用更多异步(例如:CreateConnection()、GetAllOrders),还是 少?
- 在我把这种 生产代码?
- 是否有替代策略可以产生 类似的性能但需要更少的连接?
最佳答案
您的代码最大的问题是您从数据库中获取的数据远远多于您实际需要满足查询的数据。这被称为 extraneous fetching .
Dapper 很棒,但与 Entity Framework 和其他解决方案不同,它不是 LINQ 提供程序。您必须在 SQL 中表达整个查询,包括 WHERE
条款。 Dapper 只是帮助您将其具体化为对象。它返回 IEnumerable<T>
, 不是 IQueryable<T>
.
所以你的代码:
GetAllOrders(vendorId)
.Where(order => !order.IsCancelled)
实际上请求数据库中的所有 订单 - 而不仅仅是未取消的订单。过滤发生在内存中,之后。
同样:
order.Name = (await db.QueryAsync<string>("...")).FirstOrDefault();
...
您的查询最好包含 SELECT TOP 1
,否则您实际上会取回所有元素,只是为了扔掉除第一项以外的所有元素。
另外,考虑到您正在进行许多较小的调用来填充订单的每个部分。对于每个订单,您有 3 个额外的查询,以及 N 个额外的行。这是一个常见的反模式,称为 SELECT N+1 . 总是将整个查询表示为“粗略”操作比向数据库发出许多冗长的查询要好。这也被描述为 chatty I/O anti-pattern .
关于异步问题 - 虽然并行进行多个数据库调用本质上没有错,但这并不是您在这里所做的。由于您一直在等待每一步,所以您仍在按顺序执行操作。
好吧,至少您对每个订单都按顺序执行。您在外循环中获得了一些 并行性。但是所有内部的东西本质上都是连续的。 Task.WaitAll
将阻塞,直到所有外部任务(每个过滤的订单一个)完成。
另一个问题是当您调用 GetOpenOrders
时您不在异步上下文中首先。直到你拥有 async all the way 才意识到 async/await 的真正好处在堆栈中上下移动。我也建议你看this video series on async from Channel 9 .
我的建议是:
- 确定您需要运行的完整查询以从数据库中检索所有数据,但不要超过您实际需要的数量。
- 在 Dapper 中执行该查询。使用
Query
如果您处于同步上下文 (IEnumerable<Order> GetOpenOrders
),或使用QueryAsync
如果您处于异步上下文中(async Task<IEnumerable<Order>> GetOpenOrdersAsync
)。不要尝试在非异步上下文中使用异步查询。 - 使用 Dapper 的 multi-mapping从单个查询中检索多个对象的功能。
关于c# - 我可以异步创建多个 DBConnections 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29712029/