如何对从数据库读取的记录有效地执行复杂处理

时间:2019-05-14 08:29:41

标签: c# performance optimization parallel-processing

公平的警告:这是一个关于方法的问题,至少是良好实践的问题。这里的问题不是语法,而是方法。

我必须非常快速地处理大量记录,并向用户提供一组转换后的记录。我想知道是否有人对最有效的方法有实用的建议。

这是场景:

我需要执行一组相对简单的逻辑: 连接到数据库->读取记录->转换每个记录->将输出记录提供给使用者

此逻辑需要从库中获得-内部逻辑对使用者完全隐藏。 (消费者不知道会发生某种转换-他认为他只是在遍历一堆对象。)

通常,我将使用如下方法创建IEnumerable类:

public class TransformingReader<T> where T:class,new()
{
...
...
...

 public IEnumerator<T> GetEnumerator()
 {
      var items = _connection<dynamic>.GetData();
      foreach (var item in items)
      {
          T transformed = _complexTask.Transform(item);
          yield return transformed;
      }
 }
}

(这里使用动态类只是为了说明)

使用上面的类,消费者:

foreach(var item in new TransformingReader<TransactionAnalysis>())
{
    ...
    DoStuff(item);
    ...
}

事实:

  1. 我每天要处理数百万条记录-因此处理量是一个大问题。

  2. 用户的DoStuff()函数将需要一些时间才能完成。我没有真正的方法来预测他们的工作将有多复杂,但是与我的工作相比,它必然需要更多的IO投入。

  3. 我在一个相对受限的环境中工作-因此没有足够的可用内存,并且其他应用程序位于同一台计算机上。因此,我需要表现得负责任。 (我没有在爷爷的笔记本电脑上运行-但我仍然需要编写不贪婪的明智代码)

想法:

  1. 我想尝试并行化Transform()函数,以便可以利用DoStuff()忙于转换下一条记录的时间。这样,希望我总是(经常?)在用户要求下一个记录时准备好新记录。

  2. 我希望在消费者方面保持简单的foreach语法。消费者无需知道我正在幕后努力。

任何有关如何解决此类问题的想法都将不胜感激。具体来说,也许有一种我不知道的模式可以帮助解决这个问题?

2 个答案:

答案 0 :(得分:1)

这听起来像Produce-consumer problem

一种解决方案是创建一个用于检索和转换数据的线程,即生产者线程。然后在其他线程(可能是主线程)中,运行消费者,即用户DoStuff(item)。将有一个队列(很可能是concurrent queue)将用于在线程之间进行通信。

从用户的角度来看,您仍然可以提供数据作为枚举器,该数据将从队列中读取,在队列为空时阻塞,并在读取某个表示输入结束的预定值时终止(有时称为 poison药)。

内存占用量由队列的大小决定,因此您可以根据需要进行调整。

此模式使您可以扩大生产者和消费者的数量,因此您可以同时Transform()同时DoStuff()同时使用多个项目MERGE INTO destination dst USING source src ON ( dst.some_columns = src.some_columns ) WHEN MATCHED THEN UPDATE SET columns_to_update = dst.columns_to_update_from WHEN NOT MATCHED THEN INSERT ( some_columns, columns_to_update ) VALUES ( dst.some_columns, dst.columns_to_update_from )

根据您的描述,可能可以使用一个Parallel LINQ语句来解决您的问题(幕后使用上述解决方案的变体)。

答案 1 :(得分:1)

是的,它是生产者-消费者模式。

请参见Pipelines如何实现它。

var records = new BlockingCollection<SomeRecord>();
var outputs = new BlockingCollection<SomeResult>();

var readRecords = Task.Run(async () =>
{
    using (var conn = new SqlConnection("..."))
    {
        conn.Open();
        using (var cmd = conn.CreateCommand())
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                var record = new SomeRecord { Prop = reader.GetValue(0) };
                records.Add(record);
            }
        }
    }
});

var transformRecords = Task.Run(() =>
{
    foreach (var record in records.GetConsumingEnumerable())
    {
        // transform record
        outputs.Add(new SomeResult());
    }
});

var consumeResults = Task.Run(() =>
{
    foreach (var result in outputs.GetConsumingEnumerable())
    {
        // ...
    }
});

Task.WaitAll(readRecords, transformRecords, consumeResults);

如有必要,可以轻松增加流水线级数,增加新任务。

转换很容易并行化:

records.GetConsumingEnumerable()
       .AsParallel()
       .AsOrdered() // if you want to keep order

如果其中一项任务比其他任务快得多并且阻塞了内存,则可以限制其收集的容量:

var records = new BlockingCollection<SomeRecord>(boundedCapacity: 50);