【问题标题】:Best practice for long running SQL queries in ASP.Net MVC在 ASP.Net MVC 中长时间运行 SQL 查询的最佳实践
【发布时间】:2018-02-06 08:39:07
【问题描述】:

我有一个动作方法,它需要根据用户选择的日期完成 15~52 个长时间运行的 SQL 查询(所有这些查询都相似,每个都需要 5 秒以上才能完成)。

经过大量研究,似乎在不阻塞 ASP.Net 线程的情况下最好的方法是使用带有 SQL 查询的 async/await 任务方法:

[HttpPost]
public async Task<JsonResult> Action() {   
    // initialization stuff

    // create tasks to run async SQL queries
    ConcurrentBag<Tuple<DateTime, List<long>>> weeklyObsIdBag = 
        new ConcurrentBag<Tuple<DateTime, List<long>>>();
    Task[] taskList = new Task[reportDates.Count()];
    int idx = 0;
    foreach (var reportDate in reportDates) { //15 <= reportDates.Count() <= 52
        var task = Task.Run(async () => {
            using (var sioDbContext = new SioDbContext()) {
                var historyEntryQueryable = sioDbContext.HistoryEntries
                    .AsNoTracking()
                    .AsQueryable<HistoryEntry>();
                var obsIdList = await getObsIdListAsync(
                    historyEntryQueryable, 
                    reportDate
                );
                weeklyObsIdBag.Add(new Tuple<DateTime,List<long>>(reportDate, obsIdList));
            }
        });
        taskList[idx++] = task;
    }
    //await for all the tasks to complete
    await Task.WhenAll(taskList);

    // consume the results from long running SQL queries, 
    // which is stored in weeklyObsIdBag
}

private async Task<List<long>> getObsIdListAsync(
    IQueryable<HistoryEntry> historyEntryQueryable, 
    DateTime reportDate
) {
    //apply reportDate condition to historyEntryQueryable

    //run async query
    List<long> obsIdList = await historyEntryQueryable.Select(he => he.ObjectId)
        .Distinct()
        .ToListAsync()
        .ConfigureAwait(false);
    return obsIdList;
}

进行此更改后,完成此操作所需的时间大大减少,因为现在我可以同时执行多个 (15~52) 个异步 SQL 查询并等待它们完成,而不是按顺序运行它们。但是,用户开始遇到很多超时问题,例如:

(from Elmah error log) 
"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. 
 This may have occurred because all pooled connections were in use and max pool size was 
 reached."
"The wait operation timed out"

是线程饥饿造成的吗?我有一种感觉,我可能使用线程池中的太多线程来实现我想要的,但我认为这应该不是问题,因为我使用 async/await 来防止所有线程被阻塞。

如果事情不能以这种方式进行,那么执行多个长时间运行的 SQL 查询的最佳做法是什么?

【问题讨论】:

  • 消费者-生产者模式在这里会有所帮助 + 消息代理,例如 rabbitmq。客户端可以在处理任务时检查任务的 JS 状态。
  • Windows Services 其他长期运行任务的解决方案

标签: asp.net multithreading asynchronous async-await task-parallel-library


【解决方案1】:

考虑限制正在执行的并发任务的数量,例如:

int concurrentTasksLimit = 5;
List<Task> taskList = new List<Task>();
foreach (var reportDate in reportDates) { //15 <= reportDates.Count() <= 52
    var task = Task.Run(async () => {
        using (var sioDbContext = new SioDbContext()) {
            var historyEntryQueryable = sioDbContext.HistoryEntries
                .AsNoTracking()
                .AsQueryable<HistoryEntry>();
            var obsIdList = await getObsIdListAsync(
                historyEntryQueryable, 
                reportDate
            );
            weeklyObsIdBag.Add(new Tuple<DateTime,List<long>>(reportDate, obsIdList));
        }
    });
    taskList.Add(task);
    if (concurrentTasksLimit == taskList.Count)
    {
        await Task.WhenAll(taskList);
        // before clearing the list, you should get the results and store in memory (e.g another list) for later usage...
        taskList.Clear();
    }
}
//await for all the remaining tasks to complete
if (taskList.Any())
    await Task.WhenAll(taskList);

请注意,我将您的taskList 更改为实际的List&lt;Task&gt;,它似乎更易于使用,因为我们需要从列表中添加/删除任务。

此外,您应该在清除taskList 之前获得结果,因为您稍后会使用它们。

【讨论】:

  • 这似乎是避免过多并发任务(线程)的好方法。但这也意味着我一次只能运行有限数量的长时间运行的查询,并且完成所有查询需要更长的时间,这会导致性能不佳。用户不会对此感到高兴:(
  • @winhow 确实如此,但我建议您进行一些测试,因为我真的相信这不会降低太多性能。我曾经遇到过同样的问题,但我没有注意到这样的性能损失。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-06
  • 2018-04-10
  • 1970-01-01
  • 2011-03-21
  • 1970-01-01
相关资源
最近更新 更多