在上一篇文章中,我们解释了什么是强一致性(与最终一致性相比)。 本文是该系列的第二部分,我们将解释缺乏强一致性如何使提供良好的最终用户体验变得更加困难,可能会带来严重的工程开销,并使您更容易受到攻击。 这一部分较长,因为我们将解释不同的数据库异常,通过多个示例场景,并简要介绍哪种数据库会受到每种异常的影响。
用户体验是任何应用程序成功的驱动因素,而依赖于不一致的后端会增加提供良好体验的难度。 更重要的是,在不一致数据之上构建应用程序逻辑会导致攻击。 一篇 论文 将这类攻击称为“ACIDrain”。 他们调查了 12 个最受欢迎的自托管电子商务应用程序,并识别出至少 22 种可能的严重攻击。 其中一个网站是比特币钱包服务,由于这些攻击不得不关闭。 当您选择一个非 100% ACID 的分布式数据库时,就会有“龙”。 正如我们之前的示例中所解释的那样,由于误解、定义不明确的术语和激进的营销,工程师很难确定特定数据库提供的保证。

哪些“龙”? 您的应用程序可能会出现诸如帐户余额错误、未收到的用户奖励、两次执行的交易、乱序的消息或被违反的应用程序规则等问题。 要快速了解分布式数据库为何必要且困难,请参阅我们的 第一篇文章 或此 优秀的视频解释。 简而言之,分布式数据库是指将您的数据副本存储在多个位置的数据库,以便出于规模、延迟和可用性的考虑。
我们将介绍其中四个潜在问题(还有 更多)并使用游戏开发的示例来说明。 游戏开发很复杂,这些开发者面临着许多与现实世界中严重问题非常相似的问题。 游戏有交易系统、消息系统、需要满足条件才能获得的奖励等。 请记住,如果出现问题或看似出现问题,游戏玩家会感到多么愤怒(或高兴 🤨)。 在游戏中,用户体验至关重要,因此游戏开发者通常承受着巨大压力以确保其系统具有容错能力。
准备好了吗? 让我们深入研究第一个潜在问题!
1. 过时读取
过时读取是指返回旧数据的读取,或者换句话说,返回的数据值尚未根据最新的写入更新。 许多分布式数据库,包括使用副本进行扩展的传统数据库(阅读 第 1 部分 了解这些工作原理),都会出现过时读取。
对最终用户的影响
首先,过时读取会影响最终用户。 而且这不是单一的影响。
令人沮丧的体验和不公平的优势
想象一下,游戏中两个玩家遇到一个装满金币的宝箱。 第一个玩家从一个数据库服务器接收数据,而第二个玩家连接到另一个数据库服务器。 事件顺序如下
- 玩家 1(通过数据库服务器 1)看到并打开宝箱,取出金币。
- 玩家 2(通过数据库服务器 2)看到一个完整的宝箱,打开它,却失败了。
- 玩家 2 仍然看到一个完整的宝箱,不明白为什么它会失败。

虽然这看起来像是一个小问题,但结果是给第二个玩家带来了令人沮丧的体验。 他不仅处于劣势,而且经常会在游戏中看到一些明明存在却不存在的情况。 接下来,让我们来看一个玩家对过时读取采取行动的例子!
过时读取导致重复写入
想象一下,游戏中的一个角色试图在商店里购买盾牌和剑。 如果有多个位置包含数据,并且没有智能系统来提供一致性,那么一个节点将包含比另一个节点更旧的数据。 在这种情况下,用户可能会购买物品(这会联系第一个节点),然后检查其库存(这会联系第二个节点),但发现物品并不在其中。 用户可能会感到困惑,并认为交易没有完成。 在这种情况下,大多数人会怎么做? 他们会尝试再次购买物品。 一旦第二个节点赶上,用户已经购买了**重复的**物品,并且一旦副本赶上,他突然发现自己没有钱了,而且每件物品都有两件。 他会认为我们的游戏坏了。

(t1) – 玩家购买了盾牌和剑。 此购买交易已提交至主节点。
(r1) – 玩家加载其库存,但读取命中副本 1。 由于 (t1) 尚未复制,因此他看不到他的物品。
(rt1) – 第一次交易已复制,但为时已晚,无法对 (r1) 产生影响。
(t2) – 玩家认为他的购买尝试失败了,再次购买了剑和盾牌。
(rt2) – 第二次交易已复制。
(r2) – 玩家加载其库存,现在发现他有两把盾牌、两把剑,而且几乎没有金币了。
在这种情况下,用户花费了不想花费的资源。 如果我们在这种数据库之上编写电子邮件客户端,用户可能会尝试发送电子邮件,然后刷新浏览器,但无法检索刚刚发送的电子邮件,因此再次发送。 在这种系统之上提供良好的用户体验并实现诸如银行交易之类的安全交易非常困难。
对开发者的影响
在编码时,您始终要期望某些东西不存在(尚未存在),并相应地编码。 当读取最终一致时,编写防错代码会非常具有挑战性,用户很可能会在您的应用程序中遇到问题。 当读取最终一致时,这些问题会在您能够调查它们之前消失。 基本上,您最终会追逐鬼魂。 开发者仍然经常选择最终一致的数据库或分布式方法,因为通常需要一段时间才能注意到这些问题。 然后,一旦应用程序中出现问题,他们就会尝试变得有创意,并在其传统数据库之上构建解决方案(1,2)以解决过时读取问题。 事实上,有很多类似的指南,而且像 Cassandra 这样的数据库已经实现了一些一致性功能,这表明这些问题是真实的,而且比您想象的更频繁地出现在生产系统中。 在非一致性系统之上构建的自定义解决方案非常复杂且脆弱。 如果有数据库可以开箱即用地提供强一致性,为什么有人会经历如此麻烦呢?
出现这种异常的数据库
传统数据库(PostgreSQL、MySQL、SQL Server 等)使用主读复制通常会导致陈旧读。许多较新的分布式数据库最初也是最终一致性的,换句话说,没有针对陈旧读的保护。这是因为开发者社区强烈认为这是可扩展性的必要条件。最著名的数据库就是 Cassandra,但 Cassandra 认识到用户难以处理这种异常,并已提供 额外措施 来避免这种情况。较旧的数据库或没有设计以高效方式提供强一致性的数据库,如 Cassandra、CouchDB 和 DynamoDB,默认情况下是最终一致性的。Riak 等其他方法也是最终一致性的,但它们通过实现冲突解决系统来降低过时值的可能性,从而采用不同的路径。但是,这并不能保证您的数据安全,因为冲突解决并非万无一失。
2. 丢失写入
在分布式数据库领域,当写入同时发生时,需要做出一个重要的选择。一种选择(安全的选择)是确保所有数据库节点都可以在这些写入的顺序上达成一致。这并非易事,因为它要么需要同步时钟,这需要 特定的硬件,要么需要像 Calvin 这样的智能算法,该算法不依赖于时钟。第二种不太安全的选择是允许每个节点本地写入,然后稍后决定如何处理冲突。选择第二种选项的数据库可能会丢失您的写入。

对最终用户的影响
假设游戏中有两笔交易,我们从 11 枚金币开始,购买两件物品。首先,我们以 5 枚金币的价格购买一把剑,然后以 5 枚金币的价格购买一个盾牌,这两笔交易都指向分布式数据库的不同节点。每个节点都会读取当前值,在本例中,两个节点的值都是 11。由于它们没有意识到任何复制,因此两个节点都将决定写入 6 作为结果 (11-5)。由于第二个交易还无法看到第一个写入的值,因此玩家最终以总共 5 枚金币的价格购买了剑和盾牌,而不是 10 枚。对用户有利,但对系统不利!为了解决这种行为,分布式数据库有几种策略,有些策略比其他策略更好。

解决策略包括“最后写入者获胜”(LWW)或“最长版本历史记录”(LVH)获胜。LWW 长期以来一直是 Cassandra 的策略,并且如果您没有进行不同的配置,它仍然是默认行为。
如果我们将 LWW 冲突解决应用于前面的示例,玩家仍然会剩下 6 枚金币,但只购买了一个物品。这是一种糟糕的用户体验,因为应用程序确认了对第二个物品的购买,即使数据库不认为该物品存在于其库存中。

不可预测的安全
正如您可能想象的那样,在这样的系统之上写入安全规则是不安全的。许多应用程序依赖于后端(或者在可能的情况下直接在数据库上)的复杂安全规则来确定用户是否可以访问资源。当这些规则基于陈旧且更新不可靠的数据时,我们如何确保永远不会出现安全漏洞?想象一下,一个 PaaS 应用程序的用户打电话给他的管理员,问:“你能把这个公共组设为私有组,以便我们可以将其重新用于内部数据?”管理员执行了该操作并告诉他已完成。但是,由于管理员和用户可能在不同的节点上,因此用户可能会开始向实际上仍然是公共组添加敏感数据。
对开发者的影响
当写入丢失时,调试用户问题将是一场噩梦。假设用户报告在您的应用程序中丢失了数据,然后经过一天您才有时间进行响应。您将如何尝试找出问题是由于您的数据库还是由于应用程序逻辑错误造成的?在一个允许跟踪数据历史记录的数据库中,例如 FaunaDB 或 Datomic,您将能够追溯时间,查看数据是如何被操纵的。不过,这两个数据库都不容易受到丢失写入的影响,而容易受到这种异常影响的数据库通常没有时间旅行功能。
容易受到丢失写入影响的数据库
所有使用冲突解决而不是冲突避免的数据库都会丢失写入。Cassandra 和 DynamoDB 默认使用最后写入者获胜 (LWW);MongoDB 曾经使用 LWW,但后来 放弃了 LWW。传统数据库(如 MySQL)中的主-主分布方法提供不同的冲突解决策略。许多没有为一致性而构建的分布式数据库都会受到丢失写入的影响。Riak 最简单的冲突解决是通过 LWW 驱动的,但它们也实现了更智能的系统。但即使有智能系统,有时也根本没有明显的方法来解决冲突。Riak 和 CouchDB 将选择正确写入的责任交给了客户端或应用程序,允许它们手动选择要保留哪个版本。
由于分布很复杂,大多数数据库使用不完美的算法,因此在许多数据库中,当节点崩溃或出现网络分区时,丢失写入很常见。即使 MongoDB 不进行写入分布(写入到一个节点),在节点在写入后立即宕机的罕见情况下,也可能出现写入冲突。
3. **写入倾斜**
写入倾斜是可能发生在数据库供应商称为快照一致性的一类保证中的问题。在快照一致性中,事务会从事务开始时拍摄的快照中读取数据。快照一致性可以防止许多异常。事实上,许多人认为它完全安全,直到出现证明相反的论文(PDF)为止。因此,开发者难以理解为什么某些保证不够好也就不足为奇了。
在我们讨论快照一致性中哪些不起作用之前,让我们先讨论哪些有效。假设我们有一个骑士和一个魔法师之间的战斗,他们各自的生命力量由四个心脏组成。
当任何角色受到攻击时,事务是一个计算被移除的心脏数量的函数。
damageCharacter(character, damage) {
character.hearts = character.hearts - damage
character.dead = isCharacterDead(character)
}
并且,在每次攻击之后,另一个 isCharacterDead
函数也会运行,以查看角色是否还有心脏。
isCharacterDead(character) {
if ( character.hearts <= 0 ) { return true }
else { return false }
}
在一个简单的情况下,骑士的攻击从魔法师身上移除了三个心脏,然后魔法师的法术从骑士身上移除了四个心脏,将他自己的生命值恢复到四个。如果一个事务在另一个事务之后运行,这两个事务在大多数数据库中会正常运行。

但如果我们添加第三个事务,即来自骑士的攻击,并且它与魔法师的法术同时运行呢?

骑士死了,魔法师还活着吗?
为了处理这种混乱,快照一致性系统通常会实施一个称为“第一个提交者获胜”的规则。一个事务只有在另一个事务没有写入同一行的情况下才能结束,否则它将回滚。在本例中,由于两个事务都试图写入同一行(魔法师的健康值),因此只有生命汲取法术会生效,而骑士的第二次攻击将回滚。最终结果将与前面的示例相同:一个死去的骑士和一个满血的魔法师。
但是,一些数据库,如 MySQL 和 InnoDB,不将“第一个提交者获胜”视为快照隔离的一部分。在这种情况下,我们会遇到一个 **丢失写入**:魔法师现在死了,虽然他应该在骑士的攻击生效之前获得生命汲取法术带来的健康值。(我们之前提到了定义不明确的术语和松散的解释,对吧?)
快照一致性**包含**“首个提交者获胜”规则,在很长一段时间内被认为是一个很好的解决方案,因此它在某些方面表现良好。PostgreSQL、Oracle 和 SQL Server 仍然采用这种方法,但它们对它的称呼各不相同。PostgreSQL 将这种保证称为“可重复读”,Oracle 称之为“可串行化”(根据我们的定义这是错误的),而 SQL Server 则称之为“快照隔离”。难怪人们在这个术语森林中迷路了。让我们看看它在哪些情况下表现不符合预期!
对最终用户的影响
接下来的战斗将是两支军队之间的战斗,一支军队如果所有军队角色都死亡则被认为是死亡的。
isArmyDead(army){
if (<all characters are dead>) { return true }
else { return false }
}
每次攻击后,以下函数会确定角色是否死亡,然后运行上面的函数查看军队是否死亡。
damageArmyCharacter(army, character, damage){
character.hearts = character.hearts - damage
character.dead = isCharacterDead(character)
armyDead = isArmyDead(army)
if (army.dead != armyDead){
army.dead = armyDead
}
}
首先,角色的体力会根据受到的伤害减少。然后,我们通过检查每个角色是否耗尽体力来验证军队是否死亡。如果军队的状态发生了变化,我们会更新军队的“死亡”布尔值。

有三个法师,他们分别攻击一次,导致三个“生命汲取”事务。快照是在事务开始时拍摄的,因为所有事务同时开始,所以快照是相同的。每个事务都拥有数据的副本,其中所有骑士仍然拥有全部生命值。

让我们看看第一个“生命汲取”事务是如何解决的。在这个事务中,法师1 攻击了骑士1,骑士失去了 4 点生命值,而攻击的法师恢复了全部生命值。该事务判断骑士的军队没有死亡,因为它只能看到一个快照,其中两个骑士仍然拥有全部生命值,而一个骑士已经死亡。其他两个事务作用于另一个法师和骑士,但过程类似。每个事务最初都在其数据副本中拥有三个活着的骑士,并且只看到一个骑士死亡。因此,每个事务都判断骑士的军队仍然活着。
当所有事务都完成后,没有一个骑士还活着,但表示军队是否死亡的布尔值仍然设置为 false。为什么?因为在拍摄快照时,没有一个骑士死亡。所以每个事务都看到了自己的骑士死亡,但不知道军队中的其他骑士。虽然这在我们系统中是一个异常(称为写偏斜),但写入成功执行,因为它们分别写入不同的角色,并且对军队的写入从未更改。酷,我们现在有了一个幽灵军队!
对开发者的影响
数据质量
如果我们想确保用户拥有唯一的用户名怎么办?我们创建用户的事务会检查用户名是否存在;如果不存在,我们会以该用户名写入一个新用户。但是,如果两个用户尝试使用相同的用户名注册,快照将不会注意到任何异常,因为用户被写入不同的行,因此不会发生冲突。我们现在系统中有两个拥有相同用户名的用户。
由于写偏斜而可能发生的异常还有很多。如果你感兴趣,Martin Kleppman 的著作“设计数据密集型应用程序”中描述了更多内容。

不同方式编码以避免回滚
现在,让我们考虑一种不同的方法,攻击不是针对军队中的特定角色。在这种情况下,数据库负责选择应该首先攻击哪个骑士。
damageArmy(army, damage){
character = getFirstHealthyCharacter(knight)
character.hearts = character.hearts - damage
character.dead = isCharacterDead(character)
// ...
}
如果我们像之前的示例那样并行执行多次攻击,getFirstHealthyCharacter
将始终以相同的骑士为目标,这将导致多个事务写入同一行。这将被“首个提交者获胜”规则阻止,该规则将回滚其他两个攻击。虽然这可以防止异常,但开发人员需要了解这些问题并以创造性的方式解决它们。但是,如果数据库可以开箱即用地为你完成这些操作,难道不是更容易吗?
容易出现写偏斜的数据库
任何提供快照隔离而不是可串行化的数据库都可能出现写偏斜。有关数据库及其隔离级别的概述,请参考这篇文章文章。
4. 乱序写入
为了避免丢失写入和陈旧读取,分布式数据库的目标是实现“强一致性”。我们提到数据库可以选择达成全局顺序(安全选择)或选择解决冲突(导致丢失写入的选择)。如果我们决定使用全局顺序,这意味着虽然剑和盾牌是并行购买的,但最终结果应该表现得好像我们先购买了剑,然后购买了盾牌。这也称为“线性化”,因为你可以线性化数据库操作。线性化是确保数据安全的黄金标准。
不同的供应商提供不同的隔离级别,你可以在这里比较它们。经常出现的一个术语是可串行化,它是强一致性(或线性化)的稍微不那么严格的版本。可串行化已经相当强大,涵盖了大多数异常,但仍然为由于写入重新排序而产生的一个非常细微的异常留下了空间。在这种情况下,数据库可以自由地在事务提交后切换该顺序。简单地说,线性化是可串行化加上保证的顺序。当数据库缺少这种保证的顺序时,你的应用程序容易受到乱序写入的影响。
对最终用户的影响
对话重新排序
如果有人因为错误发送了第二条消息,对话可能会以令人困惑的方式排序。

用户操作重新排序
如果我们的玩家有 11 枚金币,并且只是按重要性顺序购买物品,而没有积极地检查他拥有的金币数量,那么数据库可以重新排序这些购买订单。如果他没有足够的钱,他本可以先购买最不重要的物品。

在这种情况下,数据库会进行检查,以验证我们是否有足够的黄金。想象一下,我们没有足够的钱,而让账户余额低于零会花费我们钱,就像银行在你低于零时向你收取透支费一样。你可能会快速出售一件物品,以确保你有足够的钱购买所有三件物品。但是,旨在增加余额的出售可能会被重新排序到事务列表的末尾,这将有效地将你的余额推到零以下。如果是银行,你可能会产生你绝对不应有的费用。
不可预测的安全
配置安全设置后,用户会期望这些设置适用于所有即将进行的操作,但当用户通过不同的渠道互相交流时,就会出现问题。请记住我们讨论过的示例,其中管理员正在与用户通话,该用户想要将一个组设为私有,然后向其中添加敏感数据。虽然这种情况发生的时限在提供可串行化的数据库中变得越来越小,但这种情况仍然可能发生,因为管理员的操作可能要等到用户操作完成后才会完成。当用户通过不同的渠道交流并期望数据库实时排序时,就会出现问题。
如果用户由于负载均衡被重定向到不同的节点,这种异常也会发生。在这种情况下,两个连续的操作最终会出现在不同的节点上,并且可能会被重新排序。如果一个女孩将她的父母添加到一个具有有限查看权限的 Facebook 群组中,然后发布她的春假照片,这些照片可能仍然会出现在她父母的动态中。
在另一个示例中,一个自动交易机器人可能具有诸如最高买入价格、支出限制和关注的股票列表等设置。如果用户更改了机器人应该购买的股票列表,然后更改了支出限制,他将不会高兴,因为这些事务被重新排序,交易机器人将使用新分配的预算购买旧股票。
对开发者的影响
漏洞利用
一些漏洞利用依赖于交易的潜在逆转。想象一下,一名游戏玩家在拥有 1000 金币后立即获得奖杯,而他非常想要这个奖杯。游戏通过将多个容器中的金币加在一起来计算玩家的钱数,例如他的仓库和他的随身携带物品(他的背包)。如果玩家快速地在仓库和背包之间交换钱,他实际上可以欺骗系统。
在下图中,第二名玩家充当同伙,以确保仓库和背包之间的资金转移在不同的交易中发生,从而增加了这些交易被路由到不同节点的可能性。现实世界中更严重的例子是使用第三个账户转账的银行;银行可能会错误地计算某人是否符合贷款资格,因为各种交易被发送到不同的节点,并且没有足够的时间来进行排序。

遭受乱序写入的数据库
任何不提供线性化的数据库都可能遭受写倾斜。有关哪些数据库提供线性化的概述,请参阅此文章。剧透:没有那么多。
当一致性受限时,所有异常情况都可能返回
要讨论的最后一个对强一致性的放松是只在某些范围内保证它。典型的范围是数据中心区域、分区、节点、集合或行。如果你在限制强一致性的数据库之上进行编程,那么你需要牢记这些限制,以避免再次意外打开潘多拉的盒子。
下面是一个一致性的例子,但只保证在一个集合内。下面的例子包含三个集合:一个用于玩家,一个用于铁匠铺(即为玩家修理物品的铁匠),另一个用于物品。每个玩家和每个铁匠都有一个指向物品集合中物品的 ID 列表。

如果你想在两个玩家之间交易盾牌(例如,从 Brecht 到 Robert),那么一切都会很好,因为你仍然在一个集合中,因此你的交易仍然在保证一致性的范围内。但是,如果 Robert 的剑在铁匠铺里进行修理,而他想要取回它怎么办?然后该交易跨越了两个集合,即铁匠的集合和玩家的集合,保证失效。这种限制通常存在于文档数据库(例如 MongoDB)中。然后,你将需要改变你的编程方式来找到绕过这些限制的创造性解决方案。例如,你可以在物品本身编码物品的位置。

当然,真实的游戏很复杂。你可能希望能够将物品掉落在地板上或放置在市场上,以便物品可以归玩家所有,但不一定在玩家的背包中。当事情变得更复杂时,这些解决方法将显着增加技术深度,并改变你编写代码的方式,以保持在数据库保证的范围内。

结论
我们已经看到了当你的数据库行为不符合预期时可能出现的一些问题的不同示例。虽然有些情况最初看起来微不足道,但它们都对开发人员的生产力有重大影响,尤其是在系统扩展时。更重要的是,它们让你容易受到不可预测的安全漏洞的攻击,这些漏洞可能会对你的应用程序的声誉造成不可弥补的损害。
我们讨论了几种一致性级别,但现在我们已经看到了这些例子,让我们把它们放在一起。
陈旧读取 | 丢失写入 | 写倾斜 | 乱序写入 | |
---|---|---|---|---|
线性化 | 安全 | 安全 | 安全 | 安全 |
可串行化 | 安全 | 安全 | 安全 | 不安全 |
快照一致性 | 安全 | 安全 | 不安全 | 不安全 |
最终一致性 | 不安全 | 不安全 | 不安全 | 不安全 |
还要记住,这些正确性保证中的每一个都可能带有边界
行级边界 | 数据库提供的保证仅在事务读取/写入一行时才会生效。例如,将物品从一个玩家移动到另一个玩家的操作会导致问题。HBase 是一个将保证限制在一行的数据库示例。 |
集合级边界 | 数据库提供的保证仅在事务读取/写入一个集合时才会生效。例如,两个玩家之间的物品交易会保留在“玩家”集合中,但玩家与市场等另一个集合中的实体之间的交易会再次打开出现异常的大门。Firebase 是一个将正确性保证限制在集合中的示例。 |
分片/副本/分区/会话边界 | 只要一个事务只影响一台机器或一个分片上的数据,保证就成立。当然,这在分布式数据库中不太实用。Cassandra 最近开始提供可串行化功能,如果你配置它们,它们只在一个分区内有效。 |
区域边界 | 一些数据库几乎完全提供跨多个节点(分片/副本)的保证,但如果你的数据库分布在多个区域,它们的保证就不再成立。一个这样的例子是Cosmos。Cosmos 是一项很棒的技术,但他们选择了一种方法,其中一致性保证仅限于一个区域。 |
最后,请意识到,我们只提到了几个异常情况和一致性保证,而实际上还有更多。对于感兴趣的读者,我强烈推荐 Martin Kleppman 的设计数据密集型应用程序。
我们生活在一个我们不再需要关心的时代,只要我们选择一个没有限制的强一致性数据库。借助Calvin(FaunaDB)和Spanner(Google Spanner,FoundationDB)等新方法,我们现在拥有多区域分布式数据库,可以提供极低的延迟并在每个场景中按预期执行。那么,你为什么还要冒险自食其果,选择一个不能提供这些保证的数据库呢?
在本系列的下一篇文章中,我们将讨论对你的开发人员体验的影响。为什么说服开发人员一致性很重要如此困难?剧透:大多数人需要体验它才能看到其必要性。但请考虑一下:“如果出现错误,是你的应用程序错了,还是数据错了?你怎么知道?”一旦你的数据库的限制表现为错误或糟糕的用户体验,你就需要绕过数据库的限制,这会导致效率低下的粘合代码,这些代码无法扩展。当然,在那时,你已经投入了很多,而且认识到的太晚了。
感觉有点短视。ACID 合规性在这个时代并不能解决这个问题,因为最终一致性问题可能与分布式系统后端的数据库无关。很多时候,这与消息延迟、排队等有关,这些事情不会消失,就是这样。通常,权衡(尤其是在大型或复杂系统中)是严重的延迟。优秀的后台工程师会尽一切可能推迟任何请求的一部分,以便他们能够尽快返回给调用者,从而导致最终一致性,这就是其他模式出现的原因。前端开发人员需要接受这种现实并开始开发、记录和传播模式来应对这种现实。大型科技公司已经解决了这个问题,并以多种方式解决了这些问题,是的,这更难做,而且还有很多次要问题随之而来,这就是我们被高薪雇用的原因。