我可以异步创建多个 DBConnections 吗?

Can I create many DBConnections asynchronously?

我正在尝试提高复杂数据库读取操作的性能。我发现一些代码,在有限的测试中,执行速度比以前使用各种技术(包括手动调整的存储过程)的尝试快得多。它正在使用 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;
    }
}

这有些简化,但我希望它能突出我的主要问题:

我知道可以通过重复使用同一个连接来提高安全性,但在我的测试中创建多个连接会使速度提高一个数量级。我还 tested/counted 来自数据库本身的并发连接数,我同时看到数百个语句 运行。

一些相关问题:

您的代码最大的问题是您从数据库中获取的数据比满足查询实际需要的数据多得多。这被称为 extraneous fetching

Dapper 很棒,但与 Entity Framework 和其他解决方案不同,它不是 LINQ 提供程序。您必须在 SQL、 中表达整个查询,包括 WHERE 子句 。 Dapper 只是帮助您将其具体化为对象。它 returns 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. It is always better to express the entirety of your query as a "chunky" operation than to emit many chatty queries to the database. This is also described as the chatty I/O anti-pattern.

关于异步问题 - 虽然并行进行多个数据库调用本身并没有错,但这并不是您在这里所做的。由于您正在等待沿途的每一步,因此您仍在按顺序执行操作。

好吧,至少您对每个订单都按顺序进行。您在外循环中获得 一些 并行度。但是所有内部的东西本质上都是连续的。 Task.WaitAll 将阻塞,直到所有外部任务(每个订单过滤一个任务)完成。

另一个问题是,当您首先调用 GetOpenOrders 时,您不在异步上下文中。只有 async all the way up and down the stack. I also suggest you watch this video series on async from Channel 9.

才能实现 async/await 的真正好处

我的建议是:

  • 确定您运行从数据库中检索所有数据所需的完整查询,但不要超过您实际需要的数量。
  • 在 Dapper 中执行该查询。如果您处于同步上下文 (IEnumerable<Order> GetOpenOrders) 中,请使用 Query,如果您处于异步上下文 (async Task<IEnumerable<Order>> GetOpenOrdersAsync) 中,则使用 QueryAsync。不要尝试从非异步上下文使用异步查询。
  • 使用 Dapper 的 multi-mapping 功能从单个查询中检索多个对象。