正在查询正在增长的数据集

Querying a growing data-set

本文关键字:数据集 查询      更新时间:2023-10-16

我们有一个在应用程序处理数据集时增长的数据集。经过长时间的讨论,我们决定现在不需要阻塞或异步API,我们将定期查询我们的数据存储。

我们考虑了两个选项来设计一个API来查询我们的存储:

  1. 查询方法返回数据的快照和指示天气的标志,我们可能有更多的数据。当我们完成对最后一个返回的快照的迭代时,我们再次查询以获得其余数据的另一个快照
  2. 查询方法在数据上返回一个"活动"迭代器,当迭代器前进时,它返回以下选项之一:data is available,No more data,May have more data

我们使用C++,我们借用了.NET样式的枚举器API,原因超出了这个问题的范围。下面是一些代码来演示这两个选项。你更喜欢哪个选项?

/* ======== FIRST OPTION ============== */
// similar to the familier .NET enumerator.
class IFooEnumerator
{
    // true --> A data element may be accessed using the Current() method
    // false --> End of sequence. Calling Current() is an invalid operation.
    virtual bool MoveNext() = 0;
    virtual Foo Current() const = 0;
    virtual ~IFooEnumerator() {}
};
enum class Availability
{
    EndOfData,
    MightHaveMoreData,
};
class IDataProvider
{
    // Query params allow specifying the ID of the starting element. Here is the intended usage pattern:
    //  1. Call GetFoo() without specifying a starting point. 
    //  2. Process all elements returned by IFooEnumerator until it ends.
    //  3. Check the availability. 
    //     3.1 MightHaveMoreDataLater --> Invoke GetFoo() again after some time by specifying the last processed element as the starting point
    //                                    and repeat steps (2) and (3)   
    //     3.2 EndOfData --> The data set will not grow any more and we know that we have finished processing.
    virtual std::tuple<std::unique_ptr<IFooEnumerator>, Availability> GetFoo(query-params) = 0;
};

/* ====== SECOND OPTION ====== */
enum class Availability
{
    HasData,
    MightHaveMoreData,
    EndOfData,
};

class IGrowingFooEnumerator
{
    // HasData:
    //      We might access the current data element by invoking Current()
    // EndOfData:
    //      The data set has finished growing and no more data elements will arrive later
    // MightHaveMoreData:
    //      The data set will grow and we need to continue calling MoveNext() periodically (preferably after a short delay)
    //      until we get a "HasData" or "EndOfData" result.
    virtual Availability MoveNext() = 0;
    virtual Foo Current() const = 0;
    virtual ~IFooEnumerator() {}
};
class IDataProvider
{
    std::unique_ptr<IGrowingFooEnumerator> GetFoo(query-params) = 0;
};

更新

鉴于目前的答案,我有一些澄清。争论主要集中在接口上——它在表示不断增长的数据集的查询时的表现力和直观性,而这些数据集在某个时间点将停止增长。由于以下属性,在没有竞争条件的情况下(至少我们相信是这样)实现这两个接口是可能的:

  1. 如果迭代器+标志对表示查询时系统的快照,则可以正确实现第一个选项。获取快照语义不是问题,因为我们使用数据库事务
  2. 第二个选项可以在正确执行第一个选项的情况下执行。第二个选项的"MoveNext()"将在内部使用类似于第一个选项的东西,并在需要时重新发出查询
  3. 数据集可以从"可能有更多数据"更改为"数据结束",但不能反之亦然。因此,如果我们因为比赛条件而错误地返回"可能有更多数据",我们只会得到一小部分性能开销,因为我们需要再次查询,下次我们将收到"数据结束"

"一段时间后再次调用GetFoo(),将最后处理的元素指定为起点"

你打算怎么做?如果它使用先前返回的IFooEnumerator,那么在功能上这两个选项是等效的。否则,让调用方销毁"枚举器",然后不管调用GetFoo()多久都继续迭代,这意味着您将失去监视客户端对查询结果的持续兴趣的能力。现在你可能不需要这样做,但我认为在整个结果处理过程中排除跟踪状态的能力是糟糕的设计。

这实际上取决于整个系统是否能正常工作(不涉及实际实现的细节):

  1. 无论你如何扭曲它,在检查"是否有更多数据"和向系统添加更多数据之间都会存在竞争条件。这意味着尝试捕获最后几个数据项可能毫无意义
  2. 您可能需要限制"是否有更多数据"的重复运行次数,否则您可能会陷入"处理最后一批数据时出现新数据"的无休止循环
  3. 知道数据是否已经更新是多么容易——如果所有更新都是"新项目",并且新ID按顺序更高,你可以简单地查询"X上方有数据吗",其中X是你的最后一个ID。但如果你正在计算数据中有多少项目的属性Y设置为值A,并且数据可以在数据库中的任何地方更新(例如,出租车目前所在的数据库,每隔几秒钟通过GPS更新一次,并且有数千辆车,可能很难确定自上次读取数据库以来,哪些车已经更新)

至于您的实现,在选项2中,我不确定您所说的MightHaveMoreData状态是什么意思——要么有,要么没有,对吧?在这种情况下,重复轮询更多的数据是一种糟糕的设计,因为你永远无法100%确定从获取最后一个数据到处理并采取行动(显示、用于在股市上购买股票、停止火车或处理完新数据后你想做的任何事情)的时间内没有提供"新数据"。

读写锁可能会有所帮助。许多读卡器可以同时访问数据集,而只有一个写入器。想法很简单:-当您需要只读访问时,读取器使用"读取块",它可以与其他读取共享,也可以与写入程序独占;-当您需要写访问权限时,编写器会使用对读写器和编写器都是独占的写锁;