用于操作大量数据的数据结构和技术(100万记录和更多)

本文关键字:100万 记录 技术 用于 数据 数据结构 操作 | 更新日期: 2023-09-27 18:32:15

我一直在开发的WPF .NET 4.5应用程序,最初用于处理较小的数据量,现在适用于100万甚至更多的大数据量,当然我开始耗尽内存。数据来自MS SQL DB,数据处理需要加载到本地数据结构中,因为该数据随后由CLR中的代码转换/处理/引用,因此需要连续且不间断的数据访问,但是并非所有数据都必须立即加载到内存中,而仅在实际访问时加载。举一个小例子,反距离插值器使用此数据生成插值地图,并且需要将所有数据传递给它以进行连续网格生成。

我已经重写了应用程序的某些部分来处理数据,例如在任何给定时间仅加载x行数,并实现有效的数据处理滑动窗口方法。但是,对应用程序的其余部分执行此操作需要一些时间投入,我想知道是否有一种更强大和标准的方式来解决这个设计问题(必须有,我不是第一个)?

TLDR;C# 是否提供任何数据结构或技术来以中断方式访问大量数据,因此它的行为类似于 IEnumerable,但在实际访问或需要数据之前不会在内存中,还是完全由我来管理内存使用情况?我的理想结构是自动实现类似缓冲区的机制,并在访问该数据时加载更多数据,并从已访问且不再感兴趣的数据中释放内存。就像一些带有内部缓冲区的数据表一样?

用于操作大量数据的数据结构和技术(100万记录和更多)

就遍

历太大而无法放入内存的非常大的数据集而言,您可以使用生产者-消费者模型。当我使用包含数十亿条记录的自定义数据集时,我使用了这样的东西 - 总共大约 2 TB 的数据。

这个想法是拥有一个同时包含生产者和使用者的类。创建类的新实例时,它会启动一个生产者线程,该线程填充受约束的并发队列。该线程使队列保持已满。使用者部分是允许您获取下一条记录的 API。

从共享并发队列开始。我喜欢.NET BlockingCollection。

下面是一个读取文本文件并维护 10,000 行文本队列的示例。

public class TextFileLineBuffer
{
    private const int QueueSize = 10000;
    private BlockingCollection<string> _buffer = new BlockingCollection<string>(QueueSize);
    private CancellationTokenSource _cancelToken;
    private StreamReader reader;
    public TextFileLineBuffer(string filename)
    {
        // File is opened here so that any exception is thrown on the calling thread. 
        _reader = new StreamReader(filename);
        _cancelToken = new CancellationTokenSource();
        // start task that reads the file
        Task.Factory.StartNew(ProcessFile, TaskCreationOptions.LongRunning);
    }
    public string GetNextLine()
    {
        if (_buffer.IsCompleted)
        {
            // The buffer is empty because the file has been read
            // and all lines returned.
            // You can either call this an error and throw an exception,
            // or you can return null.
            return null;
        }
        // If there is a record in the buffer, it is returned immediately.
        // Otherwise, Take does a non-busy wait.
        // You might want to catch the OperationCancelledException here and return null
        // rather than letting the exception escape.
        return _buffer.Take(_cancelToken.Token);
    }
    private void ProcessFile()
    {
        while (!_reader.EndOfStream && !_cancelToken.Token.IsCancellationRequested)
        {
            var line = _reader.ReadLine();
            try
            {
                // This will block if the buffer already contains QueueSize records.
                // As soon as a space becomes available, this will add the record
                // to the buffer.
                _buffer.Add(line, _cancelToken.Token);
            }
            catch (OperationCancelledException)
            {
                ;
            }
        }
        _buffer.CompleteAdding();
    }
    public void Cancel()
    {
        _cancelToken.Cancel();
    }
}

这就是它的骨架。您需要添加一个 Dispose 方法,该方法将确保线程终止并关闭文件。

我已经在许多不同的程序中使用了这种基本方法,效果很好。您必须进行一些分析和测试,以确定应用程序的最佳缓冲区大小。您需要足够大的东西来跟上正常的数据流并处理突发活动,但不要太大以至于超出内存预算。

IE可进行无数修改

如果你想支持 IEnumerable<T> ,你必须做一些小的修改。我将扩展我的示例以支持IEnumerable<String>

首先,您必须更改类声明:

public class TextFileLineBuffer: IEnumerable<string>

然后,您必须实现 GetEnumerator:

public IEnumerator<String> GetEnumerator()
{
    foreach (var s in _buffer.GetConsumingEnumerable())
    {
        yield return s;
    }
}
IEnumerator IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

有了它,您可以初始化该事物,然后将其传递给任何需要IEnumerable<string>的代码。所以它变成了:

var items = new TextFileLineBuffer(filename);
DoSomething(items);
void DoSomething(IEnumerable<string> list)
{
    foreach (var s in list)
        Console.WriteLine(s);
}

@Sergey 生产者-消费者模型可能是您最安全的解决方案(由 Jim Mischel 提出),以实现完全的可扩展性。

但是,如果您要增加大象的空间(使用非常适合的视觉隐喻),那么动态压缩是一个可行的选择。使用时解压,使用后丢弃,将核心数据结构压缩在内存中。 显然,这取决于数据 - 它适合压缩的程度,但是在大多数数据结构中都有很大的空间。 如果你对某些元数据有ON和OFF标志,这可以隐藏在16/32位数字的未使用位中,或者至少保存在位而不是字节中;使用经度/经度的16位整数,并具有恒定的比例因子,以便在使用前将每个整数转换为实数;字符串可以使用 winzip 类型库进行压缩 - 或索引,以便只保留一个副本,内存中不存在重复项等。

即时减压(尽管是定制的)可以快如闪电。

我承认,整个过程可能非常费力,但随着大象的成长,绝对可以保持房间足够大 - 在某些情况下。 (当然,如果数据只是无限增长,它可能永远不够好)

编辑:重新任何来源...嗨@Sergey,我希望我能!!真正! 我已经使用这种技术进行数据压缩,实际上整个事情都是在白板上定制设计的,涉及一两个编码人员。
它当然不是(全部)火箭科学,但完全排除所有数据的性质是件好事,然后你知道(例如)某个数字永远不会超过 9999,所以你可以选择如何以最小位存储它,然后将剩余的位(假设 32 位存储)分配给其他值。 (一个真实世界的例子是一个人拥有的手指数量......松散地说,您可以将上限设置为 8 或 10,尽管 12 是可能的,甚至 20 也是远程可行的,如果他们有额外的手指,等等。 你可以明白我的意思)Lat/Longs是永远不会跨越逻辑边界的数字的完美例子(除非你使用环绕值...... 也就是说,它们总是在 -90 到 +90 之间(只是猜测哪种类型的 Lat Longs) - 这很容易减少/转换,因为值的范围非常整齐。

因此,我们没有"直接"依赖任何第三方文献。 仅在为特定类型的数据设计的算法上。

在其他项目中,对于快速实时DSP(处理),更聪明(经验丰富的游戏程序员)编码器会将浮点数转换为16位整数,并计算全局比例因子,以提供您正在收集的特定数据流(加速度计,LVDT,压力表等)的最大精度。

这减少了传输和存储的数据,而不会丢失任何信息。 同样,对于实时波/信号数据,您可以使用(快速)傅里叶变换将噪声波转换为其幅度、相位和频谱分量 - 实际上是数据值的一半,而不会实际丢失任何(重要)数据。 (在这些算法中,数据"丢失"是完全可测量的 - 因此您可以决定是否实际上丢失了数据)

同样,也有像降雨分析这样的算法(与降雨无关,更多关于周期和频率),这会大大减少您的数据。 峰值检测和矢量分析对于其他一些信号就足够了,这基本上会丢弃大约 99% 的数据......列表是无穷无尽的,但该技术必须与您的数据密切相关。 您可能有许多不同类型的数据,每种数据都适合不同的"还原"技术。 我相信你可以谷歌"无损数据缩减"(尽管我认为无损这个词是由音乐处理创造的,有点误导,因为数字音乐已经失去了高频的上限和下限......我离题了)....请发布您的发现(当然,如果您有时间/倾向进一步研究)

我有兴趣讨论您的元数据,也许可以非常优雅地"减少"一大块......