将所有内容与CRDT合成:本地第一开发
#kotlin #android #distributedsystems #localfirstdev

因此,您想构建本地第一个应用程序吗?您希望该应用程序能够离线和在线无缝地工作,而最终用户没有中断?

这有多难吗?...

A Dalle-2 generated image of a monkey trying to solve an impossible puzzle

在过去的几个月中,我一直在这个领域的图书馆工作,我认为这是简化当地第一发展及其提出的挑战的途径。但是在我介绍Synk之前,我认为它值得探索这个问题空间,看看为什么它特别棘手以及现有的解决方案。这篇博客文章对于希望进入本地第一开发和CRDT的任何人都应该是一个很好的入门。

本地第一应用程序脱机工作,它们孤立地工作,因此不需要连接到Internet即可运行。从技术角度来看,这是与传统客户端服务器应用程序的差异。

为了从网络分离,客户设备必须能够计算服务器通常派生的所有状态...这是客户端上的很多业务逻辑。所以客户发胖,那呢?好吧,除了您刚刚决定创建一个分布式系统,随之而来的是计算机科学问题的整个领域!

但是,等等是怎么发生的?

让我解释一下,在传统的客户端服务器体系结构中,服务器是福音,其保留的状态是事实和完整。我们会跳过以下事实:大多数服务器都是隐藏更深层分布式系统的外墙,我们施放的分销网仅扩展到服务器基础架构,而不是客户本身。尽管在分布式系统中,在单个节点(如服务器)中找不到状态的总数,状态是在系统中所有节点上发现的状态的汇总。既然客户端应用程序可以从网络隔离(重要的是集中式服务器状态),那么我们无疑已涉水到分布式水域中。

多数民众赞成! ...定理

好吧,我们说本地的第一个应用程序实际上是复杂的分布式系统,我们可能应该对哪种类型进行分类。如果您曾经在DS上的CS演讲中感到高兴,那么您可能会遇到CAP定理。简而言之,分布式系统最多可以具有以下三个属性中的两个:

属性 描述
一致性 每个读取都会收到最新的写或错误。
可用性 每个请求都会收到(非错误)响应,而无需保证它包含最新写作。
分区公差 尽管节点之间的网络删除(或延迟),该系统仍继续运行

Image description

具有上述属性的最多两种意味着我们最终都具有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 !!

A Dalle-2 generated image of Ali G replicating himself

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

它代表“最后写胜利地图”,这是一个密钥值映射,并有几个规定。

  1. 值必须是元组,其中一个项目始终是时间戳。在地图插入中,如果密钥已经存在,则将现有的时间戳与新的时间戳进行比较,而胜利者是最新的。

  2. 您无法删除条目,这通常是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地图呢?让我们看一个例子

fn
id ln 年龄
1 鲍勃 史密斯 35
2 杰克 琼斯 28

上表中的行无法满足LWW-MAP的条件,因为它们缺少相关的时间戳数据。因此,让我们补充一点

fn
id fn_ts ln ln_ts 年龄 age_ts
1 鲍勃 12345 史密斯 12345 35 12345
2 杰克 12345 琼斯 12345 28 12345

请注意,上表中的ID是唯一没有时间戳元的列,这是因为它充当地图中的密钥并且是不可变的,不可变的值不会改变,因此不需要元。对于其余的,如果我们检查当前的时间戳上的时间戳,并使用它来确定新值和时间戳,则我们的行作为LWW地图,将表变成CRDT。

但是我需要删除东西吗? ð

我以为你会说,我有一个解决方案,它不完美,但它有效。

墓碑

墓碑也被称为分布式系统领域之外的软删除标志。这是一个相当简单的概念,添加一个标志以表示是否已删除了记录,请在随后的查询中使用相同的标志来忽略已删除的记录。

fn
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解决您的冲突的想法激起了您的兴趣。

我选择在此博客中省略了许多细节,而不是其他任何东西。但是,如果您有兴趣,有关存储库的更多信息!我学到了很多构建这个项目,希望这个博客会教您一两件事,如果您有任何疑问可以随时发表评论。

参考

  1. 以上所有的巨大灵感是詹姆斯·朗(James Longs)演讲CRDTs for mortals,如果您还没有看到它,请观看它很棒的

所有图像都是由Dalle-2生成的,您可以看到我在下面使用的提示

图像 提示
的4个数据库的油画非常努力地彼此同步
猴子的油画试图解决最复杂的难题
ali g将自己复制成多人,作为油画