비동기 적으로 많은 DBConnection을 생성 할 수 있습니까?

asynchronous c# dapper sql-server

문제

나는 복잡한 데이터베이스 읽기 작업의 성능을 향상 시키려고 노력하고있다. 제한된 테스트에서 손으로 튜닝 된 저장 프로 시저를 비롯한 다양한 기술을 사용하는 이전의 시도보다 훨씬 빠른 코드를 발견했습니다. 그것은 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) 이하를 사용해야합니까?
  • 이런 종류의 코드를 제작하기 전에 어떤 종류의 테스트를해야합니까?
  • 유사한 성능을 낼 수 있지만 연결이 더 적은 대체 전략이 있습니까?

수락 된 답변

코드의 가장 큰 문제점은 실제로 쿼리를 만족시키는 데 필요한 것보다 데이터베이스에서 더 많은 데이터를 가져 오는 것입니다. 이는 외부 가져 오기 라고도합니다.

Dapper는 훌륭하지만 Entity Framework 및 다른 솔루션과 달리 LINQ 공급자가 아닙니다 . WHERE 절을 포함하여 SQL에서 조회의 전체를 표현해야합니다. Dapper는 객체로 구체화하는 데 도움이됩니다. IQueryable<T> 아닌 IEnumerable<T> 반환합니다.

그래서 코드 :

GetAllOrders(vendorId)
    .Where(order => !order.IsCancelled)

실제로 취소되지 않은 데이터베이스뿐 아니라 데이터베이스의 모든 주문을 요청합니다. 나중에 필터가 메모리에서 발생합니다.

마찬가지로:

order.Name = (await db.QueryAsync<string>("...")).FirstOrDefault();

쿼리의 ... SELECT TOP 1 포함하는 것이 더 좋으며, 실제로 모든 항목을 다시 가져오고 첫 번째 항목을 제외한 모든 항목을 버립니다.

또한 주문의 각 세그먼트를 채우기 위해 더 작은 호출을 많이하고 있다고 생각하십시오. 각 주문에는 3 개의 추가 쿼리와 N 개의 추가 행이 있습니다. 이것은 SELECT N + 1 로 알려진 일반적인 안티 패턴입니다. 데이터베이스에 많은 수다스러운 쿼리를 내보내는 것보다 쿼리의 전체를 "chunky"작업으로 표현하는 것이 항상 좋습니다. 이것은 또한 채팅 가능한 I / O 방지 패턴으로 설명 됩니다.

비동기식 질문과 관련하여, 병렬로 여러 데이터베이스 호출을하는 것은 본질적으로 잘못된 것은 아니지만, 여기서 정확히 무엇을하는지는 아닙니다. 당신이 길을 따라 각 단계를 기다리고 있기 때문에, 당신은 여전히 ​​일을 연속적으로하고 있습니다.

글쎄요, 적어도 당신은 각 주문에 대해 순차적으로하고 있습니다. 외부 루프에서 일부 병렬 처리가 발생합니다. 그러나 내부의 모든 것들은 근본적으로 연속적입니다. Task.WaitAll 은 모든 외부 작업 (필터링 된 주문 당 하나의 작업)이 완료 될 때까지 차단됩니다.

또 다른 문제는 처음에 GetOpenOrders 를 호출 할 때 비동기 컨텍스트에 있지 않다는 것입니다. async / await의 실질적인 이점은 스택을 위아래로 비동기 적으로 유지해야 실현됩니다. 또한 채널 9에서 비동기로이 비디오 시리즈 를 시청하는 것이 좋습니다.

내 권고 사항은 다음과 같습니다.

  • 데이터베이스에서 모든 데이터를 검색하기 위해 실행해야하는 전체 쿼리를 결정하지만 실제로 필요한 것 이상은 아닙니다.
  • Dapper에서 해당 쿼리를 실행합니다. 사용하여 Query 당신이 (동기 맥락에서 인 경우 IEnumerable<Order> GetOpenOrders ), 또는 사용 QueryAsync 당신이 (비동기 상황에 있다면 async Task<IEnumerable<Order>> GetOpenOrdersAsync ). 비동기 컨텍스트에서 비동기 쿼리를 사용하지 마십시오.
  • Dapper의 다중 매핑 기능을 사용하면 단일 쿼리에서 여러 개체를 검색 할 수 있습니다.


아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow