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

时间:2015-04-18 01:53:24

标签: c# sql-server asynchronous dapper

我正在尝试提高复杂数据库读取操作的性能。我发现一些代码在有限的测试中比以前尝试使用各种技术(包括手动调整的存储过程)执行得更快。它使用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),或 少?
  • 在我这样做之前,我可以/应该做什么样的测试 生产中的代码?
  • 是否有可以产生的替代策略 类似的性能,但需要更少的连接?

1 个答案:

答案 0 :(得分:8)

您的代码最大的问题是您从数据库中获取的数据多于实际需要满足查询的数据。这称为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功能从单个查询中检索多个对象。