You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

127 lines
12 KiB
Markdown

2 years ago
# 14 | 答疑篇:分布式事务与分布式锁相关问题
你好,我是聂鹏程。
到目前为止“分布式技术原理与算法解析”专栏已经更新13篇文章了主要与你介绍了“分布式起源”“分布式协调与同步”和“分布式资源管理与负载调度”。
在这里,我首先要感谢你们在评论区留下的一条条精彩留言。这让我感觉到,你们对分布式技术的浓厚兴趣,以及对我和对这个专栏的支持。这不但活跃了整个专栏的氛围、丰富了专栏的内容,也让我备受鼓舞,干劲儿十足地去交付更高质量的文章。
比如,@xfan、@Jackey等同学积极思考问题并对文中有疑惑的地方结合其他资料给出了自己的思考和建议再比如@zhaozp 、@静水流深等同学,每次更新后都在坚持打卡学习;再比如,@约书亚等同学针对分布式事务提出了非常好的问题,@每天晒白牙等同学对文中内容进行了非常好的总结。
这些同学有很多,我就不再一一点名了。感谢你们的同时,我也相信,积极参与并留言也会帮助你更深入地理解每一个知识点。所以我希望,你接下来可以多多留言给我,让我们一起学习,共同进步。
留言涉及的问题有很多,但我经过进一步地分析和总结后,发现针对分布式事务和分布式锁的问题比较多,同学们的疑惑也比较多。
确实,这两大问题属于分布式技术的关键问题。因此,今天的这篇答疑文章,我就围绕这两大问题来进行一次集中的分析和回答吧。
我们先来看一下分布式事务的相关问题。
## 分布式事务的相关问题
在第6篇文章[“分布式事务All or Nothing”](https://time.geekbang.org/column/article/144970)中我介绍了两阶段提交协议和三阶段提交协议。有很多同学提出了疑问两阶段提交协议2PC和三阶段提交协议3PC的区别到底是什么
在回答这个问题前,我建议你先回到[第6篇文章](https://time.geekbang.org/column/article/144970),去回忆一下它们的流程。然后,我们看看**2PC和3PC的第一步到底是不是“类似”的**
2PC的第一步投票voting阶段中参与者收到事务执行询问请求时就执行事务但不提交而3PC却写着在PreCommit阶段执行事务不提交。2PC和3PC的第一步是非常不类似吧
其实,我说它们类似,是指它们均是通过协调者,来询问参与者是否可以正常执行事务操作,参与者也都会给协调者回复。
* 在2PC中如果所有参与者都返回结果后会进入第二阶段也就是提交阶段也可以说是执行阶段根据第一阶段的投票结果进行提交或取消。
* 在3PC中进入真正的提交阶段前还会有一个预提交阶段这个预提交阶段不会做真正的提交而是会将相关信息记录到事务日志中当所有参与者都返回Yes消息后才会真正进入提交阶段。
这样说明后,相信你对这个问题的疑惑应该解决了吧。
现在,我们继续延展一下这个问题吧。
**追问1**3PC在预提交阶段才开始执行事务操作那协调者发送CanCommit给参与者的时候参与者根据什么返回Yes或者No消息呢?
3PC在投票阶段CanCommit阶段协调者发送CanCommit询问后参与者会根据自身情况比如自身空闲资源是否足以支撑事务、是否会存在故障等预估自己是否可以执行事务但不会执行事务参与者根据预估结果给协调者返回Yes或者No消息。
**追问2**3PC出现的目的是解决2PC的同步阻塞和数据不一致性问题。那么我们不可以在2PC中直接去解决这些问题吗3PC多了预提交和超时机制就真的解决这些问题了吗
**我们先来看看同步阻塞的问题。**
在2PC中参与者必须等待协调者发送的事务操作指令才会执行事务比如提交事务或回滚等操作如果协调者故障也就是说参与者无法收到协调者的指令了那么参与者只能一直等待下去。这就好比在一个班级里面班主任是协调者学生是参与者班主任告诉学生今天下午6点组织一个比赛但班主任今天生病了根本到不了学校并且也无法发送信息告诉学生那么学生们就只能一直等待。
3PC在协调者和参与者中都引入了超时机制2PC只是在协调者引入了超时也就是说当参与者在一定时间内没有接收到协调者的通知时会执行默认的操作从而减少了整个集群的阻塞时间。这就好比班主任生病了学生默认等待半个小时如果班主任还没有任何通知那么默认比赛取消学生可以自由安排做自己的事情去了。
但其实阻塞在实际业务中是不可能完全避免的。在上面的例子中学生等待超时的半个小时中其实还是阻塞的只是阻塞的时间缩短了。所以相对于2PC来说3PC只是在一定程度上减少或者说减弱了阻塞问题。
**接下来,我们再看看数据不一致的问题吧。**
通过上面的分析可以看到同步阻塞的根本原因是协调者发生故障想象一下比如现在有10个参与者协调者在发送事务操作信息的时候假设在发送给了5个参与者之后发生了故障。在这种情况下未收到信息的5个参与者会发生阻塞收到信息的5个参与者会执行事务以至于这10个参与者的数据信息不一致。
3PC中引入了预提交阶段相对于2PC来讲是增加了一个预判断如果在预判断阶段协调者出现故障那就不会执行事务。这样可以在一定程度上减少故障导致的数据不一致问题尽可能保证在最后提交阶段之前各参与节点的状态是一致的。
所以说3PC是研究者们针对2PC中存在的问题做的一个改进虽然没能完全解决这些问题但也起到了一定的效果。
在实际使用中通常采用多数投票策略来代替第一阶段的全票策略类似采用Raft算法选主的多数投票策略即获取过半参与者的投票数即可。关于Raft算法的选主的具体原理你可以再回顾下第4篇文章“[分布式选举:国不可一日无君](https://time.geekbang.org/column/article/143329)”中的相关内容。
**追问33PC也是只有一个协调者为什么就不会有单点故障问题了**
首先,我先明确下这里所说的单点故障问题。
因为系统中只有一个协调者那么协调者所在服务器出现故障时系统肯定是无法正常运行的。所以说2PC和3PC都会有单点故障问题。
但是3PC因为在协调者和参与者中都引入了超时机制可以减弱单点故障对整个系统造成的影响。为什么这么说呢
因为引入的超时机制参与者可以在长时间没有得到协调者响应的情况下自动将超时的事务进行提交不会像2PC那样被阻塞住。
好了以上就是关于分布式事务中的2PC和3PC的相关问题了相信你对这两个提交协议有了更深刻的认识。接下来我们再看一下分布式锁的相关问题吧。
## 分布式锁的相关问题
在第7篇文章[“分布式锁:关键重地,非请勿入”](https://time.geekbang.org/column/article/145505)后的留言中,我看到很多同学都问到了分布式互斥和分布式锁的关系是什么。
我们先来回顾下,分布式互斥和分布式锁分别是什么吧。
在分布式系统中,某些资源(即临界资源)同一时刻只有一个程序能够访问,这种排他性的资源访问方式,就叫作**分布式互斥。**这里,你可以再回顾下[第3篇文章](https://time.geekbang.org/column/article/141772)中的相关内容。
**分布式锁**指的是,在分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。这里,你可以再回顾下[第7篇文章](https://time.geekbang.org/column/article/145505)中的相关内容。
分布式锁的目的是,保证多个进程访问临界资源时,同一时刻只有一个进程可以访问,以保证数据的正确性。因此,我们可以说分布式锁是实现分布式互斥的一种手段或方法。
除了分布式互斥和分布式锁的关系外很多同学都针对基于ZooKeeper和基于Redis实现分布式锁提出了不少好问题。我们具体看看这些问题吧。
**首先我们来看一下基于ZooKeeper实现分布式锁的问题。**有同学问ZooKeeper分布式锁可能存在多个节点对应的客户端在同一时间完成事务的情况吗
这里我需要先澄清一下ZooKeeper不是分布式锁而是一个分布式的、提供分布式应用协调服务的组件。基于ZooKeeper的分布式锁是基于ZooKeeper的数据结构中的临时顺序节点来实现的。
请注意这里提到了ZooKeeper是一个分布式应用协调服务的组件。比如在一个集中式集群中以Mesos为例Mesos包括master节点和slave节点slave节点启动后是主动去和master节点建立连接的但建立连接的条件是需要知道master节点的IP地址和状态等。而master节点启动后会将自己的IP地址和状态等写入ZooKeeper中这样每个slave节点启动后都可以去找ZooKeeper获取master的信息。而每个slave节点与ZooKeeper进行交互的时候均需要一个对应的客户端。
这个例子说明了存在多个节点对应的客户端与ZooKeeper进行交互。同时由于每个节点之间并未进行通信协商且它们都是独立自主的启动时间、与ZooKeeper交互的时间、事务完成时间都是独立的因此存在多个节点对应的客户端在同一时间完成事务的这种情况。
接下来我们看一下基于Redis实现分布式锁的问题。**Redis为什么需要通过队列来维持进程访问共享资源的先后顺序**
在我看来,这是一个很好的问题。
@开心小毛认为Redis的分布式锁根本没有队列收到setnx返回为0的进程会不断地重试直到某一次的重试成为DEL命令后第一个到达的setnx从而获得锁至于此进程在等待获得锁的众多进程中是不是第一个发出setnx的Redis并不关心。
其实,客观地说,这个理解是合情合理的,是我们第一反应所能想到的最直接、最简单的解决方法。可以说,这是一种简单、粗暴的方法,也就是获取不到锁,就不停尝试,直到获取到锁为止。
但你有没有想过当多个进程频繁去访问Redis时Redis会不会成为瓶颈性能会不会受影响。带着这个疑惑我们来具体看看基于Redis实现的分布式锁到底需不需要队列吧。
如果没有队列维护多进程请求,那我们可以想到的解决方式,就是我刚刚和你分析过的,通过多进程反复尝试以获取锁。
但,这种方式有三个问题:
* 一是,反复尝试会增加通信成本和性能开销;
* 二是,到底过多久再重新尝试;
* 三是,如果每次都是众多进程进行竞争的话,有可能会导致有些进程永远获取不到锁。
在实际的业务场景中,尝试时间的设置,是一个比较难的问题,与节点规模、事务类型均有关系。
比如节点规模大的情况下如果设置的时间周期较短多个节点频繁访问Redis会给Redis带来性能冲击甚至导致Redis崩溃对于节点规模小、事务执行时间短的情况若设置的重试时间周期过长会导致节点执行事务的整体时间变长。
基于队列来维持进程访问共享资源先后顺序的方法中,当一个进程释放锁之后,队列里第一个进程可以访问共享资源。也就说,这样一来就解决了上面提到的三个问题。
## 总结
我针对前面13篇文章留言涉及的问题进行了归纳总结从中摘取了分布式事务和分布式锁这两个知识点串成了今天这篇答疑文章。
今天没来得及和你扩展的问题,后续我会再找机会进行解答。最后,我要和你说的是,和我一起打卡分布式核心技术,一起遇见更优秀的自己吧。
篇幅所限,留言区见。
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!