因此,您想构建本地第一个应用程序吗?您希望该应用程序能够离线和在线无缝地工作,而最终用户没有中断?
这有多难吗?...
在过去的几个月中,我一直在这个领域的图书馆工作,我认为这是简化当地第一发展及其提出的挑战的途径。但是在我介绍Synk之前,我认为它值得探索这个问题空间,看看为什么它特别棘手以及现有的解决方案。这篇博客文章对于希望进入本地第一开发和CRDT的任何人都应该是一个很好的入门。
本地第一应用程序脱机工作,它们孤立地工作,因此不需要连接到Internet即可运行。从技术角度来看,这是与传统客户端服务器应用程序的差异。
为了从网络分离,客户设备必须能够计算服务器通常派生的所有状态...这是客户端上的很多业务逻辑。所以客户发胖,那呢?好吧,除了您刚刚决定创建一个分布式系统,随之而来的是计算机科学问题的整个领域!
但是,等等是怎么发生的?
让我解释一下,在传统的客户端服务器体系结构中,服务器是福音,其保留的状态是事实和完整。我们会跳过以下事实:大多数服务器都是隐藏更深层分布式系统的外墙,我们施放的分销网仅扩展到服务器基础架构,而不是客户本身。尽管在分布式系统中,在单个节点(如服务器)中找不到状态的总数,状态是在系统中所有节点上发现的状态的汇总。既然客户端应用程序可以从网络隔离(重要的是集中式服务器状态),那么我们无疑已涉水到分布式水域中。
多数民众赞成! ...定理
好吧,我们说本地的第一个应用程序实际上是复杂的分布式系统,我们可能应该对哪种类型进行分类。如果您曾经在DS上的CS演讲中感到高兴,那么您可能会遇到CAP定理。简而言之,分布式系统最多可以具有以下三个属性中的两个:
属性 | 描述 |
---|---|
一致性 | 每个读取都会收到最新的写或错误。 |
可用性 | 每个请求都会收到(非错误)响应,而无需保证它包含最新写作。 |
分区公差 | 尽管节点之间的网络删除(或延迟),该系统仍继续运行 |
具有上述属性的最多两种意味着我们最终都具有CA,CP或AP的系统。您能猜测哪些属性在本地第一应用程序中不存在? ...
这是一致性,我们无法保证强大的一致性,因为我们希望它们离线工作,如果他们不能与系统中的其他节点协调,他们可能无法返回最新的写作。取而代之的是,一旦他们重新连接并同步了最新的更改,它们就是我们所说的“ 最终一致”。我认为放弃C是一种祝福的解决方案,可以保证强大的一致性涉及复杂的共识算法...如果您曾经学习过Paxos或Raft,那么您会知道为什么我说这是一件好事。
好吧,我们是AP,本地的第一个应用程序是AP分布式系统,很酷,这意味着什么?这意味着我们将面临一定的问题,我们可以依靠解决方案的一部分。让我们看看我们将要面对的问题类型。
问题 | 描述 |
---|---|
复制 | 变化如何在不会丢失的情况下传播给其他客户? |
冲突解决 | 两个设备在离线时会更改同一片段数据时会发生什么?谁的变化获胜,为什么? |
因果排序 | 谁在什么时候改变什么?您真的可以确定活动的时间表吗?如果客户端上的本地时钟不超出合成?..糟糕,我的意思是同步 |
剧透! Synk关注底部的两个。毕竟,您很可能已经下定了要如何传播数据的决定,例如,您想在应用程序中使用的是哪种传输(HTTP,RPC,套接字)和序列化格式。因此,复制就在您身上...但是我们有其余的解决方案。现在是时候介绍AP分布式系统中最常见的解决方案之一,这是您需要复制的东西。
CRDT
“它是cee arr dee tees而不是serdts”
CRDT也称为无冲突复制数据类型是我们将使用AP分布式系统中实现“最终一致性”的机制。
简单地说,它们是无法冲突的数据结构,每个数据结构(并且有很多!)都有一种算法,可以确定性地合并更新。对于几乎所有基本数据结构,您都可以想到(地图,集合,列表等)将存在CRDT实现,通常具有不同的怪癖,但所有保证无冲突的更新合并。
现在,如果我们要使用适当的分布式系统术语,这些更新是在节点之间发送的,以使系统最新被称为“消息”。 CRDT按其产生的消息中包含的内容进行分类。
类别 | 行为 |
---|---|
状态 | 基于状态的CRDT将发送包含CRDT当前状态的消息。这可以被认为是类似于帖子/put请求 |
操作 | 基于操作的CRDT将发送包括需要应用的操作和参数的消息。这可以被认为是类似于RPC调用。 |
delta | 基于Delta的CRDT将发送仅包含CRDT中已更改的值的消息。这可以被认为是类似于补丁请求 |
这并不是全部,CRDT是酸2.0 。如果您没有猜测,另一个首字母缩写词,让我为您分解:
- 联想
a + b = b + a
这意味着您是否将A合并到B或B中无关紧要
- 交换性
(a + b) + c = a + (b + c)
这意味着您如何将合并分组
无关紧要- disempotent
a + a + a + b = a + b
这意味着您可以多次合并相同的CRDT,但结果不会更改
- 分布式
???
老实说,这是在这里进行首字母缩写工作ð£
上面的属性非常强大,并且使AP系统中的复制工作变得更加简单。无论您是意外发送同一CRDT两次都没关系,无论您将更新发送出订单都没关系,在合并客户端C的C'之前,您将客户A的更新合并到客户端B上都没关系。将始终达到相同的确定性结果(这是我提到的总体一致的事情)。
现在您可能正在思考,这肯定很难创建满足所有这些约束的东西。但老实说,它比您想象的要简单,我将通过引擎盖下的两个Synk用途来让您看到我的意思。
首先是我最喜欢的,可以说是最简单的...
女士和先生们,请欢迎G-stet !!
g-set或仅成长的集合只是一个没有删除操作的集合,这意味着它会增长而永远不会收缩。让我们看一下它在psuedocode中的行为:
val gset = GSet<String>()
gset.add("foo")
// println(gset)
// [
// “foo”,
// ]
gset.add("foo")
gset.add("bim")
gset.add("bar")
// println(gset)
// [
// “bar”,
// “bim”,
// “foo”,
// ]
您可以从上方看到它的关联性和交换性,因为该集合具有一些内在的顺序,这使得插入/分组顺序不可知。这是一个势力,因为它是一套不允许的重复。 g-ste是符合酸2.0的,使其成为经过验证的CRDT ...
此时房间里的大象似乎并不特别有用。但是,就像计算机科学中的所有数据结构一样,CRDT可以并且应该合并在一起以建模较大的结构。 G-stet是一个集合,因此现在我们需要该集合来容纳有用的东西,另一个CRDT。
不进一步的ADO我介绍LWW-MAP
它代表“最后写胜利地图”,这是一个密钥值映射,并有几个规定。
-
值必须是元组,其中一个项目始终是时间戳。在地图插入中,如果密钥已经存在,则将现有的时间戳与新的时间戳进行比较,而胜利者是最新的。
-
您无法删除条目,这通常是CRDT中反复出现的主题,但很快就解决了。
您可以从以下psuedocode了解其行为:
val map = LWWMap()
map["foo"] = Pair(“Ali G”, 123)
// println(map)
// {
// “foo” : { “value”: “Ali G”, “ts”:123 },
// }
map["foo"] = Pair(“Ali G is da best”, 234)
map["foo"] = Pair(“Ali G is da best”, 234)
map["foo"] = Pair(“Ali G is not da best”, 112)
// println(map)
// {
// “foo” : { “value”: “Ali G is da best”, “ts”:234}
// }
地图最终允许我们序列化结构/数据类,因此现在将G-stet和LWW地图结合起来,我们可以建模类的集合。
詹姆斯·朗(James Long)在他的演讲CRDTs for mortals中进一步进行了观察,并说我们实际上可以在数据库中制作桌子,就像LWW-MAP的G-set一样,并进行了一些调整。与我同住,听说我很感激这是一个信仰的飞跃,但我们会贯穿它。
带有主键的表可以表现得像G-set一样长,只要我们从不删除条目,表毕竟是行的集合。但是,将一排建模为LWW地图呢?让我们看一个例子
id | ln | 年龄 | |
---|---|---|---|
1 | 鲍勃 | 史密斯 | 35 |
2 | 杰克 | 琼斯 | 28 |
上表中的行无法满足LWW-MAP的条件,因为它们缺少相关的时间戳数据。因此,让我们补充一点
id | fn_ts | ln | ln_ts | 年龄 | age_ts | |
---|---|---|---|---|---|---|
1 | 鲍勃 | 12345 | 史密斯 | 12345 | 35 | 12345 |
2 | 杰克 | 12345 | 琼斯 | 12345 | 28 | 12345 |
请注意,上表中的ID是唯一没有时间戳元的列,这是因为它充当地图中的密钥并且是不可变的,不可变的值不会改变,因此不需要元。对于其余的,如果我们检查当前的时间戳上的时间戳,并使用它来确定新值和时间戳,则我们的行作为LWW地图,将表变成CRDT。
但是我需要删除东西吗? ð
我以为你会说,我有一个解决方案,它不完美,但它有效。
墓碑
墓碑也被称为分布式系统领域之外的软删除标志。这是一个相当简单的概念,添加一个标志以表示是否已删除了记录,请在随后的查询中使用相同的标志来忽略已删除的记录。
id | fn_ts | ln | ln_ts | 年龄 | age_ts | del | del_ts | |
---|---|---|---|---|---|---|---|---|
1 | 鲍勃 | 12345 | 史密斯 | 12345 | 35 | 12345 | 0 | 12345 |
2 | 杰克 | 12345 | 琼斯 | 12345 | 28 | 12345 | 1 | 12345 |
我有99个问题,但冲突不是一个
crdts是解决困难问题的绝妙解决方案,就我个人而言,我发现它们是分布式系统域中最自然的解决方案,但是我必须承认,我对利用它们的现有库存在问题。
crdt通常以两种方式在域级别上实现,这需要对象以非常定制的(通常不是通用)数据存储的形式实现某些可合并的接口或数据级。共同的分母是一种侵入性技术,对您创建的应用程序的形状和设计产生了很大的影响。
如果不是这种情况怎么办?如果我们可以将冲突解决作为可插入的软件,该怎么办?我们可以引导现有系统以赋予它们分布式功能。
下沉
kotlin Multiplatform CRDT库。
Synk是一个基于州的CRDT库,它使用特殊类型的timestamp监视状态随时间变化,该类型能够在分布式系统中跟踪事件。 Synk以其自身的持续键值存储数据库在每个客户端上维护此数据,
重要的是要了解Synk不会存储您的数据,仅存储与之相关的时间戳。还记得我们添加到表的时间戳数据吗? Synk轨道单独的轨道,使数据层完全未被触及,从而使您可以使用与始终拥有的相同技术构建应用程序。
在其核心同步仅由两个函数组成!
Synk.outbound(new: T, old: T? = null): Message<T>
每当在本地应用程序中创建或更新一个新的记录/对象时,给Synk提供了最新版本和旧版本(如果适用),而Synk将返回您的消息。此消息需要传播给所有其他客户。
Synk.inbound(message: Message<T>, old: T? = null): T
从另一个客户端应用程序接收消息时,需要调用入站。此功能将为您执行冲突解决方案,并返回您的对象的实例。
Synk故意很小,您给它提供了对象,它给您消息,将这些消息传播到正确的客户端,您的应用程序不会有冲突,这很容易!
一个分布式数据库...但是您带上数据库
我很高兴看到人们想到了使用这项技术,CRDT空间中的创新是疯狂的。最终,我对这个创建最小库的项目有了一个想法,该项目为将本地数据库变成分布式数据库提供了工具,同时揭示了一些但不是全部分布式原始图。结果是非常新颖的东西,我希望使用CRDT解决您的冲突的想法激起了您的兴趣。
我选择在此博客中省略了许多细节,而不是其他任何东西。但是,如果您有兴趣,有关存储库的更多信息!我学到了很多构建这个项目,希望这个博客会教您一两件事,如果您有任何疑问可以随时发表评论。
参考
- 以上所有的巨大灵感是詹姆斯·朗(James Longs)演讲CRDTs for mortals,如果您还没有看到它,请观看它很棒的
所有图像都是由Dalle-2生成的,您可以看到我在下面使用的提示
图像 | 提示 |
---|---|
的4个数据库的油画非常努力地彼此同步 | |
猴子的油画试图解决最复杂的难题 | |
ali g将自己复制成多人,作为油画 |