深入理解分布式事务Percolator(一)
转载请附本文链接:https://blog.csdn.net/maxlovezyy/article/details/88572692
本人在字节跳动,欢迎加入我司,欢迎自荐,私聊即可。
概念篇
本篇是在之前一篇介绍完分布式事务的概念之后,再通过Percolator从新手视角去讨论事务和分布式事务,下一篇会对其设计细节做深度的分析和思考。
什么是分布式事务
假如你是一个先了解过分布式一致性,后接触分布式事务的人,那估计很容易混淆这两个概念。因为本质上都是为了宏观数据上的一致性,那么既然有了分布式一致性协议,比如paxos/raft,为什么还要搞分布式事务呢?一般来讲,一致性协议保证的是某一数据实体的多副本之间的一致性;事务保证的是不同数据实体之间关系的一致性。下面举各自可能面对的场景的例子来说明一下:
一致性协议
有一个逻辑数据单元,有N份数据副本,分布在不同的存储硬件上。我现在要对这个逻辑数据单元作更改,这个更改要广播到N个副本上,让它们之间达到某种意义的一直状态。比如原来的内容是1,我现在要改成2,那么这N个副本就要在满足协议要求的情况下做出相同改变。比如majority的raft就要求N/2 + 1个副本都变成2就等于达到了一致。分布式事务:事务是什么自己去了解吧
有M个逻辑单元互相通过关系R组成一个对外逻辑单元。假如拿银行来做举例,简单来说有甲和乙,银行账户里各有10块钱。甲转给乙5元:对外结果只能是甲5乙15或者甲10乙10。而不能是甲5乙10,因为这样整体来看就丢了5元。实际上银行有千千万万的账户,这些信息存储在不同的机器上,我们分布式事务要保证在分布式的情况下,各个逻辑单元之间在关系R的情况下的一致性。
显而易见的问题
前一节简单介绍了分布式事务要解决的问题。那么随之问题就来了,怎么做才能实现事务?ACID其他的都好说,就这个A即原子性怎么在分布式的情况下实现?假设单机呢?很现实的一个问题就是,我现在涉及了多行数据,他们怎么能做到一起成功或者一起失败。不宕机的情况下都好说,如果考虑宕机导致的中断呢?势必就要引入一个共通的地方去记录本次事务的进度等相关信息吧?一般在2阶段提交里这叫做协调者,那如果换成分布式,协调者要是挂了呢?
Percolator的解决方案
其核心就是2PC的增强版,通过利用client作为协调者,解决了协调者挂了对整体服务能力的影响,而在事务相关信息的一致性和持久性上充分利用了BigTable的简单事务支持以及GFS的多副本可靠性能力,另外Percolator在数据模型上是mvcc。
怎么保证的原子性
想保证多条指令作为整体执行的原子性,有单机并发编程基础的都应该知道,那就是通过加锁(原子操作不适合),要么你基于内存的mutex,要么基于文件系统的file lock,甚至于可以使用分布式锁服务。。。Percolator的做法是对数据扩展出一个lock列,对于多行数据,它随机选择一行作为主,通过BigTable的事务对这个primary主row的lock列写入信息表明持有锁,而其他的rows则随后在其lock列写入primary锁的位置信息。这样就完成了两件事:一个是把事务中的所有rows关联起来了;一个是互斥点唯一了,都在primary,如同加了分布式锁一样。另外还需要考虑一个问题,Percolator为什么不怕lock信息丢失呢?因为BigTable底层是GFS,是多副本,假定其能保证对外承诺的SLA下的可靠性,真发生了丢数据的问题那就人工处理呗。。。
具体实现
数据模型
宏观上每一行有4列,分别是key、data、lock和write,另外每一个cell的数据都有个版本号timestamp,这是mvcc必须的。每一列的作用如下:
key:唯一表示
data:数据
lock:锁标记
write:当前最新的key对应的data的版本号
流程
每一个事务开始的时候都会去授时服务获取一个start_ts。
写
定义一个辅助函数:某行数据的prewrite
- 检测write列和lock列与本次事务是否冲突,如果冲突则直接取消本次事务
- 以start_ts对该行数据的lock列写入信息并写入data
写主要分两个阶段,一个是客户端向Percolator SDK的事务对象写入mutations的阶段(Percolator SDK会缓存所有的mutations),一个是客户端本身的commit阶段(即调用Percolator SDK的事务对象的commit)。Percolator SDK中事务对象的commit分为两个阶段,即分布式事务的2PC:
- 选出primary,对primary执行prewrite,失败返回
- 对所有其他rows执行prewrite,失败返回
- 获取一个commit_ts,转到步骤4
- 检查是不是有自己时间戳加的锁,如果有对primary解锁并在write列写入版本号commit_ts的事务完成标示,内容指向start_ts的data,之后转到步骤5;如果没有自己start_ts加的锁,事务失败。
- 异步对其他rows执行4的处理
读
读本身很简单,这里就简单说了。先获取当前的时间戳start_ts,如果时间戳之内的数据有锁,则等,等到一定程度还不行就清理掉锁,执行读操作。这里不可以直接返回当前小于start_ts的最新版本数据,会造成幻读。因为如果当前读不等锁,再次读的话可能锁释放数据提交了,假如提交的时间戳小于读事务的开始时间戳,就会读到比之前读到的版本数据大但是依然小于start_ts的新数据,这样就产生了不可重复读或者幻读。
FAQ
为什么写提交的时候primary成功了其他的row异步就可以?
因为其他row的lock列都指向了primary,primary决议完怎么做之后,其他rows就都知道该怎么处理了。当其他rows面临访问的时候,如果当前有lock,则去看看primary怎么处理的进而对自己执行abort或commit(访问primary的lock和write列就知道该怎么做了)为什么client作为协调者挂了无所谓?
client挂了其事务也无非是完成/未完成的状态,无论哪种状态都不会导致数据不一致,仔细想想Percolator的lock和commit机制就都明白了。为什么读的时候如果等到一定程度还等不到无锁就可以主动清理锁以继续?
这和2其实是一个问题,只不过回答的时候额外多了一个考虑,那就是线性一致性的考虑。为什么这里不会影响到用户视角的数据的线性一致性呢?因为计算机的世界但凡涉及网络的请求都是3中情况:成功、失败和不知道。读等不及了要主动清理锁无非是下面2种情况:
a. 写的协调者挂了。那对于发起写的用户结果就是不可预知,他需要后续自己发请求确认到底执行的如何。
b. 写的协调者hang了。清理锁之后如果写协调者又活过来了,其执行commit的时候也必然失败,因为它持有的版本号的lock已经没了,只能取消,不会影响一致性。Percolator会有write skew问题吗?
会。
参考
这里没有谈Percolator里面的通知机制,没看。关于事务其实percolator的paper写的已经非常详细了,细节直接看就懂了。