事务提交 commit 会失败么_Google Percolator事务

ce6edce306df2242b9b7e15157ab9d2e.png

背景

倒排索引是Google搜索引擎中最为关键的技术之一。应对海量数据时,高效的索引创建和索引的实时更新都是必须解决的难题。Google设计了MapReduece系统解决了海量数据索引创建的问题,但MR并没有解决增量数据的实时更新问题。

因此,Google设计Percolator的初衷是:支持海量数据存储、并行随机读写、跨行事务的分布式数据库

由于Percolator构建在不支持跨行事务的BigTable之上,基于BigTable达到Percolator的设计目标便是其要解决的核心问题,本文主要描述Percolator系统中的事务相关设计。

特点

Percolator 提供了跨行、跨表的、基于快照隔离的ACID事务。

Snapshop isolation

Percolator 使用Bigtable的时间戳记维度实现数据的多版本化从而达到了snapshot isolation,优点是:

对于读:读操作都能够从一个带时间戳的稳定快照获取
对于写:较好地处理写-写冲突:若事务并发更新同一个记录,最多只有一个会提交成功

50c69e00cb30a2f0b3304009b730473f.png

快照隔离的事务均携带两个时间戳:​

(图中小空格)与​
(图中小黑球)。上图中:
  • ,所以事务1的更新对2不可见
  • 事务 3 可以看到事务 2 和 事务 1的提交信息
  • 事务 1 和 事务 2并发执行:如果两者更新同一个记录,至少有一个会失败

Lock

Percolator后端存储基于BigTable。由于Bigtable没有提供便捷的冲突解决和锁管理方案,Percolator需要实现一套独立的锁管理机制。必须满足以下条件:

能直面机器故障:若一个锁在两阶段提交时消失,系统可能将两个有冲突的事务都提交。
高吞吐量:上千台机器会同时请求获取锁。
低延时:每个读操作都需要读取一次锁

事务

存储-COLUMN

Percolator在BigTable上抽象了五个COLUMN,其中三个跟事务相关

LOCK COLUMN

事务产生的锁,未提交的事务会写本项,记录primary lock的位置。事务成功提交后,该记录会被清理。记录内容格式:

:数据的key

:事务开始时间戳

:事务primary引用。在执行Percolate事务时,会从待修改的keys中选择一个(随机选择)作为
,其余的则作为

DATA COLUMN

存储实际用户数据,数据格式为

​ :真实的key

:事务的开始时间戳

: 用户数据值

WRITE COLUMN

已提交的数据信息,存储数据所对应的时间戳。数据格式

:数据的key

:事务提交时间戳

:事务开始时间戳,可根据key + start_ts在DATA COLUMN中找到数据value

关键在于WRITE COLUMN,只有该列正确写入后,事务的修改才会真正被其他事务可见。读请求会首先在该COLUMN中寻找最新一次提交的start timestamp,这决定了接下来从DATA COLUMN的哪个key读取最新数据。

事务提交关键流程

Prewrite

9b3405e6c3c7cdf6ad759f9c3cdba386.png

Prewrite是事务两阶段提交的第一步:

  1. 客户端首先从Oracle获取全局唯一时间戳作为当前事务的​
  2. 客户端会从所有key中选出一个作为
    ​,其余的作为
    ​。并将所有的key/value数据写入请求并行地发往对应的存储节点。存储节点对key的处理如下:
1.
冲突检查:从WRITE COLUMN列中获取当前key的最新数据,若其​
大于等于
​,说明在该事务的更新过程中其他事务提交过对该key的修改,返回
WriteConflict错误
2. 检查key是否已被锁,如果是,返回 KeyIsLock的错误
3. 向LOCK COLUMN列写入
​为当前key加锁。若当前key被选为primary,​
标记为
​。若为secondary,则指向
​的信息

4. 向DATA COLUMN列写入数据​

Commit

abc3fc4495f2c4676cd472f815ba4ffa.png

Prewrite成功后,进入事务的第二阶段Commit。

  1. 从Oracle获取时间戳​作为事务的提交时间
  2. 向primary key所在的存储节点发送commit请求
  3. 步骤2正确完成后该事务即可标记为成功,接下来异步并行地向secondary keys所在的节点发送commit请求
  4. 存储节点对于客户端请求的处理:
1. 获取key的lock,检查其合法性,若非法,则返回失败
2. 将​
写入WRITE COLUMN

3. 从LOCK COLUMN中删除key的锁记录以释放锁

值得说明的是,一旦

节点提交成功后,整个事务就算提交成功了。

在某些实现中(如TiDB),Commit阶段并非并行执行,而是先向

节点发起commit请求,成功后即可响应客户端成功且后台异步地再向
发起commit。

读取

WRITE COLUMN记录了key的提交记录,当客户端读取一个key时,会从WRITE COLUMN中读根据key查找记录的

,再根据
作为新的key从DATA COLUMN中读取value。

存储节点对读请求处理流程如下:

  1. 检查​区间
    内Lock是否存在,若存在,则返回错误。在该区间内有lock意味着有未提交的事务,客户端需要等到持有该锁的事务提交了才能读取到最新的数据
  2. 如果不存在有冲突的Lock,获取WRITE COLUMN中合法的最新提交记录​
  3. 根据步骤2获取的信息,从DATA COLUMN中获取到相应的数据​

异常处理-清理锁

在Prewrite阶段检测到锁冲突时会直接报错(读时遇到锁就等直到锁超时或者被锁的持有者清除,写时遇到锁,直接回滚然后给客户端返回失败由客户端进行重试),锁清理是在读阶段执行。有以下几种情况时会产生垃圾锁:

1. Prewrite阶段:部分节点执行失败,在成功节点上会遗留锁
2. Commit阶段:Primary节点执行失败,事务提交失败,所有节点的锁都会成为垃圾锁
3. Commit阶段:Primary节点执行成功,事务提交成功,但是在secondary节点上异步commit失败导致遗留的锁
4. 客户端奔溃或者客户端与存储节点之间出现了网络分区造成无法通信

对于前三种情况,客户端出错后会主动发起Rollback请求,要求存储节点执行事务Rollback流程。这里不做描述。

对于最后一种情况,事务的发起者已经无法主动清理,只能依赖其他事务在发生锁冲突时来清理。

Percolator采用延迟处理来释放锁

事务A运行时发现与事务B发生了锁冲突,A必须有能力决定B是一个正在执行中的事务还是一个失败事务。因此,问题的关键在于如何正确地判断出LOCK COLUMN中的锁记录是属于当前正在处于活跃状态的事务还是其他失败事务遗留在系统中的垃圾记录 ?

梳理事务的Commit流程一个关键的顺序是:事务Commit时,1). 检查其锁是否还存在;2). 先向WRITE COLUMN写入记录再删除LOCK COLUMN中的记录。

假如事务A在事务B的primary节点上执行,它在清理事务B的锁之前需要先进行锁判断:如果事务B的锁已经不存在(事实上,如果事务B的锁不存在,事务A也不会产生锁冲突了),那说明事务B已经成功提交。如果事务B的primary lock还存在,说明事务没有成功提交,此时清理B的primary lock。

假如事务A在事务B的secondary 节点上执行,如果发现与事务B存在锁冲突,那么它需要判断到底是执行Roll Forward还是Roll Back动作。

判断的方法是去Primary上查找primary lock是否存在:

如果存在,说明事务B没有成功提交,需要执行Roll Back:清理LOCK COLUMN中的锁记录;
如果不存在,说明事务已经被成功提交,此时执行Roll Forward:在该secondary节点上的WRITE COLUMN写入内容并清理LOCK COLUMN中的锁记录。

几种情形分析:

节点作为Primary在事务B的commit阶段写WRITE COLUMN成功,但是删除LOCK COLUMN中的锁记录失败。如果是由于在写入过程中出现了进程退出,那么节点在重启后可以恢复出该事务并删除LOCK COLUMN
节点作为Primary在事务B的commit阶段写WRITE COLUMN失败:意味事务B提交失败,那么事务A可以直接删除事务B在LOCK COLUMN中的锁记录
节点作为Secondary在事务B的commit阶段写WRITE COLUMN成功,但是清理LOCK COLUMN锁失败,因为在事务commit的时候先向primary节点发起commit,因此,进入这里必然意味着primary节点上commit成功,即primary lock肯定已经不存在,因此,直接执行Roll Forward即可。

有一个场景值得探讨

假如事务A(清理锁)和事务B(提交)并发执行,可能出现的执行顺序是:A1->B1->A2->B2->B3,也即在事务B向WRITE COLUMN中插入记录之前其锁就被其他事务清理了,会不会出现什么问题?

4791a607fe042261b811820e21be01f0.png
可能产生的问题:如果此时有start_ts更大的读请求到来,由于事务B的锁记录已经不存在,因此它会认为事务B的WRITE COLUMN已经得到是最新内容,但是实际情况是B的WRITE COLUMN记录还未得到更新,造成了无法读取到最新的数据。

暂时还没想清楚这个问题是如何解决的?

示例

银行转账,Bob 向 Joe 转账7元。

首先​以

查询WRITE COLUMN获取最新时间戳(小于7的最新时间戳),得到
​。再从DATA COLUMN里面读取该时间戳的数据值​
,同样获取到Joe 的帐户下该时间戳下的值为
​。

b4057c0657863bfbde9be4d13416994d.png

转账开始:使用​

作为事务开始时间戳,将Bob选为本事务的
​,写入LOCK COLUMN来锁定Bob的帐户,同时将数据
​写入DATA COLUMN。

caf69e752fcce3862c807134f7d5de79.png

与此同时,使用​

锁定Joe的帐户,当前锁作为
​并存储一个指向
​的引用(当失败时,能够快速定位到​锁,并根据其状态异步清理),并将Joe改变后的余额
​写入到DATA COLUMN

f9685d17bf476c558def30e494842653.png

事务携带当前时间戳​

进入commit阶段:WRITE COLUMN列中写入记录
​,删除​
所在的LOCK COLUMN数据至此,读请求过来时将看到Bob的余额为3。

884e0cd192393f180a8106430ec11938.png

依次在​

中写入WRITE COLUMN数据项并清理锁,整个事务提交结束。在本例中,只有Joe,写入的内容为​

adf80acb7fdc3824c92ec80c941c2f4d.png

点评

Percolator的事务方案对写友好,对读不友好

事务写primary record就相当于先把协调者的决议持久化,然后再异步持久化到参与者,减少了多参与者出现异常的等待,但协议的交互轮次并未减少
对于读而言,因为持久化决议分成先写primary再写其他参与者,导致参与者的加锁时间变长了。SI隔离级别下,单机读分布式事务参与者因此会等待更长的时间。单机写的锁冲突也会加剧。

Percolator的事务方案写性能本身也不算非常理想,体现在

协议基于BigTable设计,持久化次数多
如果2pc中commit时primary出问题,其他参与者也不可用且持锁的参与者再没有可能推进,依赖其他事务的锁清理机制。

参考

Shirly's Blog​andremouche.github.io
1d806c6959859b12ed481ecfa0428067.png
LoopJump's Blog​loopjump.com
ec46bcd30709dbb34b5259878d9d40e5.png
Percolator简单翻译与个人理解​www.jianshu.com
ad558888c8c320e338e4ebd2c846626a.png
Codis作者首度揭秘TiKV事务模型,Google Spanner开源实现!​dbaplus.cn
fb4cb936a3a749041fe114f6b0b74046.png
快速理解Omid:Yahoo在HBase上的分布式事务方案 - 数据库服务器 - 最新IT资讯_电脑知识大全_网络安全教程 - 次元立方网​www.it165.net
3f878f624ab6233dc4b557dbdb702bc4.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值