学习 TLA+ - Percolator Transaction

引子

在讨论 TiKV Transaction 之前,我们先聊聊 Percolator。TiKV 的事务是参考 Google [Percolator] (后面我们使用 Percolator 来表示 Percolator 的事务)实现的,然后做了些许优化,所以这里,我们先实现 Percolator 的 TLA+,然后再去实现 TiKV 的。

介绍

Percolator 是建立在 Google BigTable 上面的,而 BigTable 只能支持单行事务,所以 Percolator 需要在 BigTable 上面封装一套事务机制来实现分布式事务。

Percolator 仍然使用的是 MVCC 机制,用来提供 Snapshot Isolation。Percolator 并不像其他传统的 DBMS 一样,直接内置 lock 来进行并发控制。Percolator 只能通过 client 来操作 BitTable,所以需要自己实现 lock,加上 Percolator 任何节点都可以发起请求,并没有传统 2 PC 里面一个协调者的概念,所以 Percolator 的整个事务其实是实现的非常巧妙和高效的。

我们先介绍下 Percolator 的几个主要 Column 定义:

  • lock:任何的未提交的事务都会写 lock。Lock 分为 primary lock 和 secondary key,如果是 secondary lock,则会包含 primary lock 的位置信息。
  • write:用来表示数据已经提交,里面会存放数据的 timestamp。
  • data:实际的数据

Percolator 还有一个 notify 和 ack 的 Column,主要是用在通知上面,但因为我们没有用到,所以这里就不做说明了。

流程

在 Percolator 论文里面对事务已经有详细的伪码实现,这里简单的用通俗的语言介绍一下。Percolator 使用的是乐观锁机制,当事务开始的时候,首先会拿一个 start timestamp,然后用这个 timestamp 去获取感兴趣的数据,然后再提交的时候进行加锁处理。

Get

首先我们来看 Get, 事务会先用 start timestamp 去拿对应 key 的数据,首先看这个 key 上面是不是有 lock,如果有 lock,可能几种情况:

  1. 另一个事务正在提交数据,这个 key 被 lock 住了,所以我需要在等待一段时间再重试。
  2. 如果很长一段时间,这个 key 还是被 lock 住的,有可能对应发起事务的 client 已经挂掉了,这时候就要做清锁处理。
  3. 这个 lock 其实是一个残留的 lock,这种情况多数发生在 一个事务 primary key 已经提交成功并清理了 primary key,但 secondary key 没有提交成功,secondary key 残留的情况,这时候其实就可以安全的对 secondary key 进行清锁处理。

当没有 lock 的时候,这时候就可以找到在 [0, start_ts] 区间里面最新的一个 committed write 的,如果没有,那就是这个 key 还没有任何数据,如果找到了,则通过 committed write 里面对应的 start timestamp 的值,找到对应的 data。

Pre-write

当事务拿完所有的数据,并处理之后,就要将更新提交了,提交分为两步,首先就是 pre-write,对于任何 key,会先判断在 [start_ts, +] 范围内是不是有一个新的 write 写入,如果有,表明另一个事务已经写入了新的数据,那么当前事务就要结束回滚了。也需要判断 key 的 lock 是不是已经被锁住,如果有,也需要结束回滚了。

当发现没有任何新的写入,锁也没有被占用的时候,我们就可以写入自己的 lock 以及对应的 data。

Commit

当事务所有的 key 都完成了 pre-write,我们就可以最终进行 commit,commit 会先提交 primary key,首先会判断 primary key 对应的 lock 还在不在,如果不在了,标明其他的事务已经把当你给清掉了,所以我们只能失败回滚。如果发现 lock 还在,则可以写入 committed write,以及将 lock 给清掉。

当 primary key 提交成功之后,我们就认为事务已经完成了,secondary key 就可以异步提交,主要就是写入 committed write 和清掉 lock。即使 secondary key 提交失败了,下一个事务如果读到了这个 key 的 lock,发现该 lock 对应的 primary key 已经清掉,就自动帮之前的事务写入 committed write 和清锁,然后在继续处理自己的事务数据了。

例子

我们来看一个简单的例子,初始的时候,Bob 是 10 块钱,而 Job 是 2 块钱,Bob 要给 Joe 转 7 块钱,那么最终结果就是 Bob 后面只有 3 块钱,Job 有 9 块钱。

我们开启一个事务,起始 timestamp 是 7。因为 Bob 和 Joe 之前的数据都是提交的,提交 timestamp 是 6,所以我们就能读到 6 对应的实际数据,也就是 timestamp 5 的 data - 10 和 2。

key bal:data bal:lock bal:write
Bob 6:
5: $10
6:
5:
6: data@5
5:
Joe 6:
5: $2
6:
5:
6: data$5
5:

因为这个事务涉及到了两个 key,也就有两个 lock,我们设置 primary lock 是 Bob ,而 secondary key 则是 Job。首先对 Bob 进行 prewrite 操作,在 timestamp 7 写入 primary lock 以及对应的 data,也就是 3 块钱。

key bal:data bal:lock bal:write
Bob 7: $3
6:
5: $10
7: I am primary
6:
5:
7:
6: data@5
5:
Joe 6:
5: $2
6:
5:
6: data$5
5:

然后对 Joe 进行 Prewrite 操作,写入 secondary key,里面的值就是对应的 Job 的 primary key,顺带写入新的 data,也就是 8 块钱。

key bal:data bal:lock bal:write
Bob 7: $3
6:
5: $10
7: I am primary
6:
5:
7:
6: data@5
5:
Joe 7: $9
6:
5: $2
7: primary@Bob.bal
6:
5:
7:
6: data$5
5:

当两个 lock 都写入成功之后,我们就需要先提交 primary key 了,这里就是 Bob,我们使用新的提交 timestamp 8,写入 write,值就是 data 对应的 timestamp,也就是 7,同时将自己的 primary lock 给清掉。

key bal:data bal:lock bal:write
Bob 8:
7: $3
6:
5: $10
8:
7:
6:
5:
8: data@7
7:
6: data@5
5:
Joe 7: $9
6:
5: $2
7: primary@Bob.bal
6:
5:
7:
6: data$5
5:

只有 primary key 提交成功,那么整个事务就是成功了,即使 secondary key 没有被清理成功,后面也会被其他事务给正常清理掉。但这里我们还是说一下正常流程,提交 secondary key,也就是 Joe 会写入 write,里面的值就是对应的 data 的 timestamp,就是 7,顺便清掉自己的 lock。

key bal:data bal:lock bal:write
Bob 8:
7: $3
6:
5: $10
8:
7:
6:
5:
8: data@7
7:
6: data@5
5:
Joe 8:
7: $9
6:
5: $2
8:
7:
6:
5:
8: data@7
7:
6: data$5
5:

进行完上面的步骤,这次事务就结束了。当然这里为了简单,我们并没有冲突,或者异常的情况。

TLA+

上面我们详细讨论了 Percolator 的算法以及一个简单的例子,剩下的就是开始使用 TLA+ 来详细的实现了。完整的 Percolator TLA+ 可以参考 [Percolator.tla],这里我只是简单的介绍一下。

常量和变量

首先我们引入两个常量 KEY 和 CLIENT,KEY 表示的就是要操作的 key 的集合,大家可以在 TLA+ Toolbox 里面设置,譬如 {"k1", "k2", "k3"}。然后就是 CLIENT,可以认为是要发起事务的客户端集合,我们可以设置为 {"c1", "c2", "c3"}。然后我们定义一个 next_ts 用来获取当前的 timestamp,因为 Percolator 能通过一个 Oracle 服务来保证 timestamp 单调递增,所以这里我们也约定不同事务的 timestamp 一定是不同,单调递增的。

对于一个实际发起事务的 client,它可能会处于多种状态,譬如 “init”,”working”,”committed”,”aborted” 等,这里我们使用变量 client_state 来表示。每个 client 会保存自己事务的 timestamp,我们使用 client_ts 来存储,里面是一个 record, 譬如 [start_ts |-> 1, commit_ts |-> 2]。然后对于一个 Percolator 事务来说,它有一个 primary key,剩下的就是 secondary key,我们使用一个 record,譬如 [primary |-> "k1", secondary |-> {"k2", "k3"}]

对于实际的 key,我们有一个 key_lock,它是一个 lock record (譬如 [start_ts |-> 1, primary |-> "k1"]) 集合,用来保存事务的 lock,还有一个 key_data,用来保存实际事务的数据,因为每个事务的 start timestamp 是唯一的,所以我们这里就使用 start timestamp 来当做 data,也就是 key_data 是一个 start timestamp 的集合。然后就是 key_write,用来保存这个 key 在不同的 timestamp 的提交,因为 write 一定是有序写入的,所以 key_write 是一个 write record(譬如 [start_ts |-> 1, commit_ts |-> 2])的 sequence。

关键流程

之前我们说过,写 TLA+,在确定好变量之后,最重要的就是需要注意:

  1. 变量的初始化
  2. 从一个状态转换到另一个状态,变量之前的关系

首先我们来看初始化,如下:

Init == 
    /\ next_ts = 0
    /\ client_state = [c \in CLIENT |-> "init"]
    /\ client_ts = [c \in CLIENT |-> [start_ts |-> 0, commit_ts |-> 0]]
    /\ client_key = [c \in CLIENT |-> [primary |-> "", secondary |-> {}]]
    /\ key_lock = [k \in KEY |-> {}]
    /\ key_write = [k \in KEY |-> <<>>]
    /\ key_data = [k \in KEY |-> {}]

在上面的 Init 函数中,我们对所有的变量进行了初始化处理,将 client 的 state 全部设置成了 “init” 状态。Client 的事务 start 和 commit timestamp 都是 0,而 primary 和 secondary key 都还没有。在 KEY 这边当然就没有任何的数据。

然后就是 Next 函数:

Next == 
    \E c \in CLIENT:
        Start(c) \/ Get(c) \/ Prewrite(c) \/ Commit(c)

对于任意的 client,我们都有可能进入 Start,Get,Prewrite,Commit 等阶段,这些其实就是对应的 Percolator 算法的具体流程了。譬如对于 Commit,实现如下:

Commit(c) ==
    /\ client_state[c] = "committing"
    /\ IF canCommitPrimary(c)
       THEN
            /\ commitPrimary(c)
            /\ client_state' = [client_state EXCEPT ![c] = "committed"] 
            /\ UNCHANGED <<key_data, client_txn_vars, next_ts>>
            \* If we commit primary successfully, we can think the transaction is committed
            \* TODO: use async message to commit second keys
       ELSE
            /\ client_state' = [client_state EXCEPT ![c] = "aborted"]      
            /\ UNCHANGED <<key_vars, client_txn_vars, next_ts>>

这里简单介绍一下,首先 client 的 state 要处于 “committing” 状态,我们才会继续处理 commit,然后 canCommitPrimary 必须要为 TRUE,这样我们就可以实际的去 commitPrimary,顺带将自己的 state 改成 “committed”,也就是事务提交成功了。这里我并没有提交 secondary,因为 Percolator 算法会保证它被后面的事务给正确提交。不过不排除我面我加上 Message Bag 来处理。如果 canCommitPrimary 不为 TRUE,那么事务就变成 “aborted” 了。

当然,写完了算法,我们还需要证明算法是可靠的,这里我写了两个 Consistency 验证,一个是 WriteConsistency,它会去验证 key_write 一定是按照 commit timestamp 有序排列的。另一个就是 CommittedConsistency,如果一个 client 处于 “committed” 状态了,那就表明这个事务提交成功了,那么它的 primary key lock 一定被清掉,write 里面一定有 commit timestamp,但 secondary key lock 不一定会被清掉,如果还有 secondary key lock,那么 key_lock 一定没有其他的 lock,同时 key_write 里面最后一次的 commit timestamp 一定要小于 lock 的 start timestamp。当然,我可能后面还会加入更多的 Consistency 验证。

具体的算法实现大家可以参考源码,因为这算是我第一次写 TLA+ 难免,可能有些地方没有写好,欢迎大家提出修改意见。

小结

当我们设计一个算法的时候,都可以使用 TLA+ 先来证明我们算法的正确性,如果算法都不确定,后面如何实现都是枉然的。当然,这里需要注意,即使 TLA+ 证明出来我们设计的算法没问题,但并代表自己实现的算法没有问题,毕竟理论论证是一回事,工程实践又是另一回事。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容