作为基于NOSQL的体系结构的新手,我有一种恼人的直觉,即我将只写一组明显的语句就什么都不会写。 另一方面,我尚未在任何地方阅读有关此内容的详细说明,因此希望它对其他一些开发人员也很有用。
问题陈述
NOSQL数据库不提供对不同数据实体(*)的多次更新操作的原子事务。 一个简单的解释是 两阶段提交机制强制每个参与者将其数据锁定一段时间,而这并不能随着高度分区的数据进行扩展(参见CAP定理)。 但是, 文档存储类型的NOSQL数据库通常能够根据需要自动更新一个文档的多个属性(对于AWS SDB数据行和MongoDB JSON文档至少是这样)。
* [2011年9月10日更新] :我最近发现了很棒的neo4j图形数据库,该数据库将自己称为“ NOSQL”,并为传统的ACID事务提供支持。 然而,即使Neo4j的可部署为一个集群中的复制模式,它是不分区宽容,如解释在这里 。 Redis还提供了一些有趣的原子事务支持 ,尽管,正如此处所指出的那样,更新操作失败不会触发其他事务的自动回滚。
当然,有很多情况需要将多个实体更新为一个操作。 在NOSQL世界中,还有更多这样的示例,因为出于性能方面的考虑,我们鼓励跨实体复制数据,而不是在读取时使用联接。
* [2011年9月10日更新] :由于所谓的n + 1问题,这些操作在大多数当前的NOSQL产品中成本很高。 使用RDBMS解决方案时,通常可以通过在返回数据给应用程序之前让数据库本身在内部执行联接来解决。 我最近了解到,一些NOSQL产品还允许DB内部关系的遍历: Neo4j的是其中一个当然的,自然的,但Redis的也提供了这样的机制,截至年底解释这个帖子 ,并在这里 。
一个简单的解决方案是忽略原子性的缺乏,并在部分更新失败的情况下简单地使数据不一致。 当数据被认为不如吞吐量重要(例如统计趋势的收集)时,或者在不一致的情况下,这意味着我们最终会遇到一些未删除的“失效”记录,这些记录将被永久忽略,这可能是可以接受的。
另一种方法是尝试通过在失败的情况下撤消已经执行的操作来进行补偿。 但是,很难以这种方式获得100%的一致性保证,因为这需要建立一个永不失败的撤消机制,这可能是一件非常困难的事情。
一个简单的例子
想象一下一个应用程序,该应用程序允许用户创建配置文件并相互发送消息。 想象一下需求告诉我们,在显示消息列表时,我们必须同时显示作者和收件人的出生城市。
为此的关系模型将仅使用外键来链接实体。
但是,在NOSQL世界中,不建议这样做,因为遍历关系需要对DB进行补充往返(我承认,在我的简单示例中,对性能的影响很小)。 因此,我们选择对数据进行非规范化,并在userMessage表中复制作者和收件人的个人详细信息。 当然,这在每次更新概要文件时都需要进行几次更新操作(因为现在还必须更新消息)。 在许多应用程序中,读操作比写操作发生的频率更高,因此这种方法的总体性能通常很好。
解决方案概述
我刚刚发现亚当·赫鲁库(Adam Heroku)在4年前已经在博客中发布了一个非常相似的解决方案(argl!),在这里:
这似乎证实了我只是在这里说明显而易见
勇敢地继续前进…
此处提出的解决方案是使用两个不同的数据空间,一个用于写入数据,一个用于读取数据。 在幕后,一项工作是异步地将数据从第一个空间拉出,并将其影响传播到第二个空间。 两个空间都有非常不同的数据模型。 在本文中,我将这些空间称为写入域和读取域 。
写作活动
如上面的问题说明所述,我们可以在一个原子操作中更新的最大数据集是一个“行”。
因此,对于每个所需的更新事务,“只写DAO”将只输出一个整体事件,详细描述刚刚发生的业务动作。 在我的示例中,这些事件将是“ ProfileUpdateEvent”的实例。 在Heroku的银行帐户示例中,这些就是“分类帐”。
事件必须是自包含的 ,即捆绑足够的信息,以便以后可以仅根据事件的内容完全评估它们对域实体的影响。 在此以后的评估中,它们将被视为过去的快照,可能难以与其他资源的当前状态相关联。
我 t是同样重要的是所产生的事件输出到DB前全面验证的商业行动 。 传播作业异步运行,并且没有简单的方法来警告更新的发起者某些输入参数不正确或不一致。
一个很好的改进是尝试在将事件插入数据库之后立即触发下一部分中描述的传播作业。 如果第二步失败,那还是可以的,因为我们知道以后无论如何都会重试。 我们甚至可以对服务策略进行适当的降级,仅当系统负载低于给定阈值时才触发此立即传播。 这样可以确保在流量较高的情况下释放计算资源(在云中是无限的,我知道,我只是不信任此^ __ ^)。
传播工作:
传播作业是一个在后台连续运行的过程,其作用非常简单:
- 从写域中读取一个事件(或多个相关事件,如果相关)
- 以幂等方式更新相应的读取域表
- 如果以上所有操作均成功,请从写入域中删除该事件
当然,这种传播涉及几个更新操作,在此再次保证不能原子地执行。 但是,由于事件应该在上一步中得到充分验证,因此此处的任何故障都应仅在可恢复错误 (例如网络中断,节点崩溃等)的情况下发生,这意味着如果我们定期重试失败的传播,应该最终成功。 如果不是这种情况,则意味着我们有一个错误,无论如何都需要人工干预。
因此, 至关重要的一点是,对读取域执行的更新操作是幂等的 ,以便我们可以重复执行完整的操作集(甚至是上次未失败的操作)。 在用户配置文件示例中可以轻松实现幂等:我们可以使用相同的参数重复执行“ setAuthorCity”方法,而不会产生副作用。 在Heroku的分类账示例中实现起来比较棘手,但仍然可行,例如,通过将“接收到的事件ID”列表记录为更新行的一部分(它必须在更新行本身内,此处再次出现,因为这是最大的行)我们可以执行的原子写操作)。
读取数据
读操作归结为,只要有业务需要,就可以从读域中读取数据。
补充步骤可以通过读取与从读取的域获得的实体相关的所有未决事件并实时计算域实体的最可能的状态来提高所获得信息的一致性。 与上面“写事件”部分中介绍的服务机制的降级相似,在应用程序流量达到峰值的情况下,可以指示已读取的DAO不要执行此附加步骤,以便返回稍差的一致性结果,但还要释放资源更快。
结论
此处提出的基于事件的存储解决方案使系统可以从NOSQL数据库提供的高可伸缩性中受益,同时仍然为任何写入数据提供100%的“最终一致性”保证,即使需要将多个更新操作作为一个事务性事务来执行单元。 该方法不会阻止我们定义非常不规范的数据模型,从而避免了联接操作并提高了读取访问速度。
解决方案的三个重要方面是
- 在放入数据库之前,必须对事件进行充分验证
- 事件必须自成体系
- 影响的传播必须以幂等的方式进行
读取和写入操作都比传统方法简单得多,因为它们处理的数据模型与各自的用例完全吻合,并且在移入/移出持久层时几乎不需要任何操作。 业务逻辑干净地封装在传播作业中,远离应用程序外部接口,因此该方法还有助于我们实施良好的架构实践。
由于可以选择立即通过写操作触发传播作业,因此输出事件仅作为确保数据最终一致性的一个小的补充步骤而出现。 因此,在正常流量下,在写入的数据对只读DAO可见之前,所需的延迟不应高于我们在NOSQL系统中已经经历的延迟。
参考:来自我们的JCG合作伙伴 Svend Vanderveken的Svend博客上 基于事务的基于事件的NOSQL存储 。
相关文章 :
翻译自: https://www.javacodegeeks.com/2011/12/transactional-event-based-nosql-storage.html