下载本文的PDF版本 PDF

所有数据库都属于我们

在大型开放的云世界中,高可用性的分布式对象将占据主导地位。


Erik Meijer,微软


在数据库领域,原始物理数据模型是宇宙的中心,查询可以自由地假定数据表示的细节(索引、统计信息、元数据)。这种封闭世界的假设以及由此产生的抽象缺乏,带来了数据可以比应用程序更长寿的愉快效果。另一方面,这使得独立于模型上的查询来演化底层模型变得困难。

随着向云端的迁移给数据库的封闭世界假设带来了压力,暴露裸数据并依赖声明式魔法变成了一种负担,而不是一种资产。在云端,角色颠倒了,对象应该隐藏其私有数据表示,仅通过定义良好的行为接口公开它。

编程语言世界一直以来都拥抱这种开放世界的假设,将行为置于宇宙的中心,并从不可靠的部件中命令式地构建可靠的应用程序。对象数据库又卷土重来了。为了帮助开发人员过渡到云端,现有的类库和工具基础设施需要发展成为高可用性服务,使用反映相关操作细节的常规面向对象编程语言接口公开。

建模者:修复表示,改变行为

数据库开发人员(我们称之为建模者)和应用程序开发人员(我们称之为程序员)对世界持有双重观点。如果你是一个建模者,那么一切都围绕着数据——以及关于数据的数据(元数据)。数据库模型的世界是基于名词的,谈论客户、订单、订单项等等。一旦建模者正确设计了数据模型,他们就认为自己的工作完成了。

在建模者的领域中,没有数据抽象的概念,可以将模型的抽象属性与完全规范化的实现细节(以具有 PK/FK(主键/外键)关系的表的形式)分开。

声明客户
  (ID int主键,…)
声明订单
  (ID int主键, CID int引用客户(ID), …)
声明订单项
  (ID int主键, OID int引用订单(ID), …)

规范化表声明

PK/FK 关系的由外而内的方面实际上放大了暴露底层行和表的所有细节的需求。例如,你不能从客户表中选择一个特定的客户行,并询问它的订单列表是什么。相反,你必须访问并理解客户订单表的模式,才能在订单表中的所有订单行和客户表中的所有行上执行连接,以检查订单中的外键是否与客户的主键匹配,然后按你的目标客户进行过滤。

建模者认为这种缺乏数据隐藏是一种优势,因为它允许对可能之前没有想到的数据进行即席查询,例如将所有客户与所有价格与其年龄匹配的产品连接起来。可以说,不施加数据抽象的优势在于数据可以比应用程序更长寿。

(数据库确实以视图、存储过程和表值函数的形式支持有限类型的数据抽象。然而,这些更像是 .NET 扩展方法,因为它们对公共数据进行操作,并且不强制数据隐藏。此外,这使概念模型复杂化,因为这些附加概念并非根植于关系代数的底层数学基础。)

这种以名词为中心的世界观延续到建模者看待计算的方式。对于他们来说,计算递归地是另一种模型,一种可以检查、转换和优化的计划,并最终由查询引擎解释。查询执行引擎通常使用恒定空间——也就是说,它解释一个固定的静态计划,而不需要调用堆栈或使用项重写来执行。甚至计算的上下文也被视为数据,通过系统目录来维护关于其他数据(例如数据库中的表及其属性)的元数据。

在执行之前将执行计划视为数据进行检查和优化的能力非常强大,但自然会导致具有有限运算符集合的一阶、非递归计算模型。建模者也喜欢绘制他们的计划图,这是一个很棒的调试工具,但在处理高阶构造(例如一等函数或嵌套结构)、复杂的控制流或任意递归时会变得困难。允许递归,以所谓的公共表表达式的形式,表达形式为X=B∪F(X)的线性递归。这对应于简单的重复,仍然可以绘制成带有循环的图片。

数据库查询是声明式的。例如,典型的 SQL 查询(例如以下查询,它计算客户的每个类别的所有产品的总价)在语义上定义了一个三重嵌套循环,该循环迭代每个客户、订单和订单项。然而,建模者相信查询优化器会找到更高效的执行计划2,该计划利用索引和其他高级技术使其快速运行。

选择客户, 订单项.类别, Sum(订单项.价格)
来自客户, 订单, 订单项
其中客户.ID = 订单.CID并且订单.ID = 订单项.OID
分组依据订单项.类别
每个类别的购买总额

这需要一个完美的封闭世界,优化器可以在查询使用的所有表上进行推理,并且建模者可以免于显式处理延迟、异常或其他低级操作问题。当表变得太大以至于无法容纳在单台机器上,或者当你尝试连接两个位于不同“世界”或管理域中的表,或者在缓慢且不可靠的网络上执行分布式事务时,这些假设就会崩溃。查询突然需要变得分区感知,从而破坏了声明式查询语言的许多优势,这种语言将“如何做”隐藏在“做什么”之下。

无法自然地执行任意计算、推理代码操作行为的困难以及缺乏数据抽象,可以说是取消了传统关系数据库技术作为云编程操作模型的资格。Ted Codd 令人惊叹的美丽数学抽象在数据库可以控制一切的微观层面上为我们服务得很好,但是当我们移动到云的宏观层面时,它们就崩溃了。正如物理学中,当你移动到亚粒子层面时,你需要用量子力学代替经典力学一样,在计算机科学世界中,当你超越单机层面时,你需要用动词代替名词。

SQL 数据库的问题不是数据模型或出色的查询处理功能,而是假设所有数据都位于同一位置并满足一系列一致性约束,这在开放的分布式世界中很难维护。构建工作系统相关的细节已经转移,这意味着抽象级别也必须随之发展。然而,在封闭世界的本地上下文中,RDMS(关系数据库管理系统)的表达能力、效率和可靠性仍然难以匹敌。

程序员:修复行为,改变表示

诸如“行为”、“命令式”、“副作用”、“任意嵌套”、“高阶函数”或“无限递归”等概念是程序员的命脉,他们的世界围绕着计算,而不是数据。对于程序员来说,一切都与动词有关,例如删除文件, OnDragDrop等等,以及通过严格分离对象的实现和接口来强制执行强大的数据抽象。

从维基百科(http://en.wikipedia.org/wiki/Object-oriented_programming)摘取一页,“对象可以被认为是将其数据包装在一组旨在确保数据得到适当使用并协助使用的函数中。对象的方法通常包括特定于对象包含的数据类型的检查和保护措施。对象还可以提供简单易用、标准化的方法来对其数据执行特定操作,同时隐藏完成这些任务的具体方法。这样,可以对对象的内部结构或方法进行更改,而无需修改程序的其余部分。”

以 C# 中的using语句为例,它获取一个可释放的资源,使用该资源执行一个具有副作用的语句,然后通过以下语法释放资源

using (varr = e) s

资源是任何实现IDisposable接口的对象,无需进一步询问。IDisposable接口有一个单一的无参数Dispose()方法,用于释放资源

interface IDisposable{voidDispose(); }

因为编译器仅假定资源实现了 IDisposable 接口,而不关心资源的具体类型,所以它可以盲目地将using语句反糖化为以下底层原始序列:赋值和一个 try-catch 块

varr = e;trysfinally(rasIDisposable).Dispose();

资源的实际实现和类型被严格保密,并且不向using语句公开。这允许相同的语法用于一系列对象,例如TextReaderIEnumerable,它们遵循相同的创建/使用/删除通用模式。

接口或类的方法实现可以使用任意命令式代码。事实上,因为每个方法都接受一个隐式的this参数,该参数包含方法本身,所以方法本质上是递归的,1代码的执行使用运行时调用堆栈,该堆栈动态跟踪递归调用,因为计算展开成潜在的无限调用树。

编译器尝试优化静态代码表示或跟踪运行时执行路径,然后再优化生成的直线代码。然而,开发人员只期望恒定时间的改进,并且通常不喜欢他们的计算结构被优化器大幅度且不可预测地重新排列,因为程序的正确性可能严重依赖于副作用导致的求值顺序。此外,程序员要求调试器能够将优化后的代码映射回他们编写的实际源代码,以便他们可以检查计算中的中间值以解决错误或问题。

指定规范和实现之间的行为契约的能力,以及方法实现可以调用任意代码的事实,使得应用程序能够比数据更长寿。当利用颠覆性技术进步(例如云)来改进给定接口的实现时,这一点尤其重要,对于程序员和应用程序来说,干扰最小。

演化集合

作为数据抽象的一个具体示例,请考虑 .NET 标准集合库System.Collections.Generic,或类似地,Java 集合库和 C++ STL 容器。这些库是在机器是单核且大多数程序是单线程时实现的。因此,逻辑权衡是不默认使集合线程安全。例如,在 .NET 泛型集合库中,类Dictionary<K,V>实现了三个(标准)集合接口

classDictionary<K,V> : IDictionary<K,V>,
  ICollection<KeyValuePair<K,V>>, IEnumerable<KeyValuePair<K,V>>
.NET 泛型字典类

程序员实际上并不关心字典类是如何实现的,只要它满足其基本接口定义的契约即可。作为奖励,因为Dictionary<K,V>实现了IEnumerable<KeyValuePair<K,V>>,你可以自动使用 LINQ to Objects 来查询字典,将其视为键值对的集合。

(由于 LINQ to Objects 的实现是根据IEnumerable接口定义的,因此它不能使用字典实现的特定功能来提高效率,这正是建模者会做的事情。然而,在实践中,LINQ to Objects 的实现通过检查已知的具体集合类型并动态调度到特定于类型的实现来“作弊”。)

随着多核处理器的出现,并发性变得司空见惯,因此,默认情况下使集合线程安全是有意义的。此外,我们希望实现 LINQ to Objects,以通过 PLINQ(并行 LINQ)最大化 LINQ“查询”中固有的并行性。为了利用多核机器,.NET Framework 4.0 在System.Collections.Concurrent命名空间中引入了集合的新实现(http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx)。猜猜新的线程安全类 ConcurrentDictionary<K,V> 实现了哪些接口?与旧的Dictionary<K,V>class

classConcurrentDictionary<K,V> : IDictionary<K,V>,
  ICollection<KeyValuePair<K,V>>, IEnumerable<KeyValuePair<K,V>>
.NET 并发字典类

完全相同。Java 也经历了完全相同的演变,其中HashMap<K,V>ConcurrentHashMap<K,V>都实现了不变的基本接口Map<K,V>.

。这是最纯粹形式的数据抽象——具有新的和改进的实现的相同接口。在这种特定情况下,集合的新实现可以将字典划分为可以并发访问和更新的独立分片,这是一个相关的实现细节,因此应该通过 .NET 框架中的Partitioner<T>OrderablePartitioner<T>抽象基类显式公开给程序员。

(请注意,正如常规 LINQ to Objects 实现通过向下转型到IEnumerable的具体实现来作弊一样,PLINQ 实现对新的并发集合应用了同样的残酷和非自然的行为。)

数据结构即服务

当我们从多核世界走向云的大规模并行和分布式世界时,System.Collections命名空间演化的下一个逻辑步骤3是添加对集合即服务的支持,方法是引入诸如DistributedDictionary<K,V>之类的新类型,这些类型使用熟悉的集合接口,但实现为在高可用性可扩展服务,运行在某些底层云结构(例如 Windows Azure、Amazon EC2(弹性计算云)或 Google AppEngine)上

classDistributedDictionary<K,V> : IDictionary<K,V>,
  IObservable<KeyValuePair<K,V>>
假设的分布式字典类

由于分布式系统(例如云)在延迟和错误条件方面的约束与在单核或多核机器上本地运行对象的约束不同,因此必须通过修改接口以使用新的Task基于异步模式(由最新版本的 C# 和 Visual Basic 中的 await 支持)来添加对异步操作的支持。对于开发人员来说,这是熟悉的接口的一个小的且非破坏性的演变,以反映底层运行时中的基本操作更改。

集合即服务非常有用。想象一下,你想构建一个基于 Web 的游戏,该游戏在数百万游戏玩家中维护一个高分榜(每个有抱负的游戏开发者都梦想像《黑手党战争》或《开心农场》一样受欢迎)。假设每个玩家维护一个System.Collections.Distributed.DistributedSortedList<int,Player>的列表实例,假设在变量highScores中。然后,你可以通过在代码中修改和调用列表来以编程方式更新和比较分数,如下所示(使用 C# 5.0 异步方法)

varhighScores = … 获取连接到服务的对象实例 …
awaithighScores.Add(100000, me);
vartop10 =awaithighScores.TakeAsync(10);
计算高分

由于该服务是高可用的,因此玩家可以随时断开连接并重新连接到它。由于它是可扩展的,因此它可以支持数百万并发玩家。当开发人员梦想平台即服务时,他们会设想一个像这样的服务类库。

请注意,我们选择在System.Collections.Distributed中使用常规编程语言 API 而不是通过 REST 或其他网络级接口来公开服务。正如我们喜欢将对象的状态隐藏在接口之后一样,对象的客户端代理用于与实现其行为的服务通信的确切协议是一个内部细节,出于相同的原因,应该保持私有。在这种情况下,有趣的是,某些 Web 浏览器通过从人类可读的 HTTP 切换到更高效的 SPDY 协议,将相同的原则应用于访问 Web 页面。大多数现代服务(例如 Twilio 和 Google Thialfi 的分布式事件通知服务)都更喜欢特定于语言的 API 而不是协议级访问。正如 Google Thialfi 团队解释的那样,“最初,我们没有任何客户端库,而是选择直接公开我们的协议。然而,工程师们强烈倾向于针对本机语言 API 进行开发。而且,高级 API 使我们能够在不修改应用程序代码的情况下演化我们的客户端-服务器协议。”

System.Collections.Distributed”的想法并非科幻小说。Java 标准集合类的分布式“直接替换”重新实现的具体示例是 KeptCollections ( https://github.com/anthonyu/KeptCollections) ,它使用 Apache ZooKeeper 作为后备存储,以提供Map<K,V>接口的分布式和可扩展实现。Redis 项目宣称自己是“数据结构服务器”,并且是提供分布式集合(例如表、列表和集合)作为服务的另一种实现。诸如 MemcacheDB 和 Windows Azure Caching and Service Bus 之类的服务是其他以问题为中心的服务示例,这些服务实现了标准集合类。

让我们回到建模者,基于 Dynamo9 的 noSQL 数据库(例如 Riak)诉诸 CAP 定理,以牺牲一致性(建模者习惯的)来换取可用性(某些应用程序(例如购物车)需要的),因为它们试图创建可以应对开放云世界的数据库。放弃一致性的后果是,现在由应用程序来解决冲突,这让许多建模者深感担忧。然而,程序员已经熟悉不一致和不可用的系统。每次你在对象上调用方法时,它都可能抛出异常,并将对象的隐藏状态置于未定义状态。你能够在网站上阅读这篇文章这一事实证明,程序员非常清楚如何使用既没有事务、也没有一致性也没有可用性的不可靠组件来构建高度可扩展、实用的分布式系统。他们从一开始就将他们的系统设计为对不一致性具有鲁棒性。

选择可用性而不是一致性的一个未充分公开的后果是,查询必须处理版本冲突。例如,由于一个键可能包含值的多个版本,因此 MapReduce 查询中的 map 函数必须解决冲突(无法写回选择),因为它应用于每个键。大多数程序员可能更喜欢放弃可用性,而不是必须解决版本控制冲突。

研究界一直在研究所谓的 CRDT(收敛或可交换 复制数据类型8)——分布式数据类型,它们满足代数属性,保证异步复制下的最终一致性。这些数据结构在程序员的便利性和实现的伸缩性之间取得了良好的平衡。该领域中同样有趣和相关的工作是关于并发修订7和较早的工作,例如 Time Warp 操作系统中的可逆计算。4

并发即服务

对于线程和并发,也可以得出与集合相同的类比。在单核机器上,程序员使用System.Threading.Thread类处理(模拟的)并发。对于多核机器,该模型扩展为System.Threading.Tasks。在这种情况下,线程和任务没有实现公共接口(尽管请参阅 Java 的 Executor 框架和 Rx 调度程序),但在概念上它们非常相似。线程和任务都是通过将操作委托传递给各自的构造函数来构造的

varthread =newThread
  (delegate{ … });

vartask =newTask(delegate{ … });

,然后通过调用它们的Start()方法来启动。任务 API 比原始线程 API 丰富得多,例如,允许使用ContinueWith方法将现有任务的共单子组合成新任务。然而,线程和任务都体现了数据隐藏的格言,即提供一组连贯的行为,这些行为代表并发的抽象概念,而不会泄露底层的实现细节,例如在幕后使用工作窃取调度算法的线程池。

云不仅有一个线程池,而且还有一个巨大的机器海洋。我们希望将所有丰富的潜在并发性作为程序员可以利用的服务公开。除了简单的操作之外,我们还希望在云池中启用可以从故障域故障以及集群重启中幸存下来的长期运行且高可用的计算。Carl Hewitt 的 Actor 是云规模并发的流行模型,Rx、Dart、Erlang、Akka、F#、Axum 和 Ciel 等语言和库证明了这一点。与建模者青睐的 MapReduce 或 Pregel 等更静态的分布式计算模型相比,Actor 允许程序员表达完全递归和动态的分布式计算。细心的读者会正确地意识到,分布式 Actor 是实现数据结构即服务以及具有固定行为的对象的自然方式。

在操作上,程序员需要处理四种基本形式的双重计算效果

1. 阻塞计算的内置效果(在 Haskell 等纯语言中,这将作为类型IO<T>出现),它同步返回单个值。

2. 类型为IEnumerable<T>的阻塞或基于拉取的集合,它同步返回多个值。

3. 类型为Task<T>Future<T>的非阻塞计算,它异步返回单个值。

4. 类型为IObservable<T>的流式传输或基于推送的集合,它异步返回多个值。

诸如线程或 Actor 以及常规方法调用之类的各种并发原语使用这四种基本效果的特定组合来生成其结果(参见表 1)。

服务即服务

在开发代码时,程序员不仅依赖于编程语言和库,还依赖于各种工具,例如调试器和性能分析器。在谈论将现有编程习惯用法迁移到云端时,我们倾向于将许多构建企业级服务所必需的细节隐藏起来,例如监视、跟踪、分布式锁定、故障检测、复制和故障转移。有趣的是,其中许多可以作为服务实现的对象公开。实现用于调试、理解和监视分布式系统的构建块并非万能药,但它是使程序员在新兴的分布式应用程序世界中获得成功的必要条件。

从较小的服务组合更大的服务时,相关的其他方面是共址和高效的服务解析。即使在单台 Windows 机器上,也需要推理线程亲缘性——例如,为了更新 UI,你必须在 UI 线程上运行。构建分布式应用程序的实用基础必须提供使位置和放置边界显式化的抽象——并且可能提供本地原子性或可靠性保证。在单台机器的范围内,程序员经常忽略这些细节(就像建模者一样),但是在分布式世界中,公开它们变得相关且重要,程序员和建模者都需要承认。

所有数据都是单子

即使我们认为模型不是云计算的正确操作基础,也绝对不要误解我们承认(关系)数据库技术的巨大经济和知识价值,并且程序员渴望使用建模者策划、清理、规范化和改进的数据。

幸运的是,可以通过诉诸 Codd 理论6 的推广——即范畴论,将模型嵌入到程序中。事实证明,各种数据库实现中的大多数“集合”或“表”概念实际上都是数学概念的实例,实际上只是一种称为单子的接口。查询可以转换为作为代码实现的底层单子运算符。

程序员称之为 LINQ5 而不是单子,并且有无数数据库(例如 SQL、Azure Tables、MongoDB、CouchDB、Hadoop 和 HBase)的 LINQ 绑定,这些绑定通过使用通用接口将模型作为推送或拉取集合公开给开发人员。

对象数据库的复仇

关系数据库和分布式 Actor 在正确的上下文中都闪耀着光芒。借用 Pat Helland10 的话来说,SQL 显然是内部数据事务处理的无可争议的领导者。1980 年代的面向对象数据库试图太像数据库而太不像对象,并且它们没有发挥自己的优势。在表、声明式查询和复杂优化器2 的小封闭世界中,RDMS 在自己的游戏中很难被击败。

然而,在大型开放的云世界中,高可用性、不一致性鲁棒、作为标准命令式对象/Actor 公开的 Web 服务将占据主导地位。云时代是(单子和共单子)计算和动词的时代,而不是数据和名词的时代。

为了方便程序员过渡到云端,我们确定了以下必备条件

• 使用单子/LINQ 向程序员公开建模者创建的每个数据源。

• 创建一个分布式集合类库,该库实现为高可用性和可扩展服务,但使用标准编程语言绑定和接口公开。

• 通过共单子/Actor 让程序员访问云中的并发海洋。

• 将跟踪、监视、调试和诊断基础设施作为云中的另一项服务公开。

许多所需的材料今天都可用。缺少的是将所有这些部分优雅而功能齐全地组装成一套软件包,以应对开发人员在迁移到云端时面临的挑战。

致谢

我要感谢 Brian Beckman、Terry Coatta、Gavin Bierman、Joe Hoag、Brian Grunkemeyer 和 Rafael Fernandez Moctezuma 改进了本文的风格和内容。

引用

1. Abadi, M., Cardelli, L. 1997. 对象理论。ECOOP(欧洲面向对象编程会议)教程; http://lucacardelli.name/Talks/1997-06%20A%20Theory%20of%20Object%20(ECOOP%20Tutorial).pdf

2. DeWitt, D. J. 2010. SQL 查询优化:为什么如此难以做好?PASS Summit 主题演讲; http://www.slideshare.net/GraySystemsLab/pass-summit-2010-keynote-david-dewitt

3. Gribble, S. D., Brewer, E. A., Hellerstein, J. M., Culler, D. 2000. 用于 Internet 服务构建的可扩展分布式数据结构。第 4 届 Usenix 操作系统设计与实现研讨会论文集; http://static.usenix.org/event/osdi00/full_papers/gribble/gribble_html/dds.html

4. Jefferson, D., et al. 1988. Time Warp 操作系统状态。《第三届超立方体并行计算机及其应用会议论文集:架构、软件、计算机系统和一般问题》1(738-744); http://users.ecs.soton.ac.uk/rjw1/misc/VirtualTime/p738-jefferson.pdf

5. Meijer, E. 2011. LINQ 的世界。《》9(8); https://queue.org.cn/detail.cfm?id=2024658

6. Meijer, E., Bierman, G. 2011. 大型共享数据库的协同关系数据模型。《》9(3); https://queue.org.cn/detail.cfm?id=1961297

7. 微软研究院。并发修订; http://research.microsoft.com/en-us/projects/revisions/default.aspx

8. Shapiro, M., Preguiça, N., Baquero, C., Zawirski, M. 2011. 收敛和可交换复制数据类型的综合研究。Inria No. RR-7506; http://hal.inria.fr/inria-00555588/en/

9. Vogels, W. 2007. 亚马逊的 Dynamo。All Things Distributed; http://www.allthingsdistributed.com/2007/10/amazons_dynamo.html

10. Helland, P. 2005. 外部数据与内部数据 (144-153); http://www.cidrdb.org/cidr2005/papers/P12.pdf

喜欢它,讨厌它?请告诉我们

[email protected]

ERIK MEIJER ([email protected], [email protected]) 在过去的15年中,他一直致力于“云的民主化”。 他最著名的或许是他对Haskell语言的研究以及他对LINQ和Rx(响应式框架)的贡献。 他是TUDelft的兼职云编程教授,并在微软服务器和工具事业部领导云可编程性团队。

© 2012 1542-7730/12/0700 $10.00

acmqueue

最初发表于Queue vol. 10, no. 7
数字图书馆中评论这篇文章





更多相关文章

钱力, Peter Kraft - 事务和无服务器天生一对
数据库支持的应用程序是无服务器计算令人兴奋的新领域。 通过紧密集成应用程序执行和数据管理,事务性无服务器平台实现了许多在现有无服务器平台或基于服务器的部署中不可能实现的新功能。


Pat Helland - 身份的各种名称
新兴的系统和协议既收紧又放宽了我们对身份的概念,这很好! 它们使完成工作变得更容易。 REST、IoT、大数据和机器学习都围绕着有意保持灵活且有时模棱两可的身份概念。 身份的概念是我们分布式系统基本机制的基础,包括可互换性、幂等性和不变性。


Raymond Blum, Betsy Beyer - 实现数字永久性
今天的信息时代正在为世界所依赖的数据创造新的用途和新的管理方式。 世界正在从熟悉的物理artifacts转向更接近信息本质的新表示方式。 我们需要流程来确保知识的完整性和可访问性,以保证历史将被知晓和真实。


Graham Cormode - 数据速写
您是否曾经感到被无休止的信息流所淹没? 似乎大量的新电子邮件和短信需要持续关注,还有电话要接听,文章要阅读,还有敲门声要回应。 将这些碎片拼凑在一起以跟踪重要内容可能是一个真正的挑战。 为了应对这一挑战,流数据处理模型越来越受欢迎。 目标不再是捕获、存储和索引每一分钟的事件,而是快速处理每个观察结果,以便创建当前状态的摘要。





© 版权所有。

© . All rights reserved.