14 答疑篇:分布式事务与分布式锁相关问题 你好,我是聂鹏程。

到目前为止,“分布式技术原理与算法解析”专栏已经更新13篇文章了,主要与你介绍了“分布式起源”“分布式协调与同步”和“分布式资源管理与负载调度”。

在这里,我首先要感谢你们在评论区留下的一条条精彩留言。这让我感觉到,你们对分布式技术的浓厚兴趣,以及对我和对这个专栏的支持。这不但活跃了整个专栏的氛围、丰富了专栏的内容,也让我备受鼓舞,干劲儿十足地去交付更高质量的文章。

比如,@xfan、@Jackey等同学积极思考问题,并对文中有疑惑的地方结合其他资料给出了自己的思考和建议;再比如,@zhaozp 、@静水流深等同学,每次更新后都在坚持打卡学习;再比如,@约书亚等同学针对分布式事务提出了非常好的问题,@每天晒白牙等同学对文中内容进行了非常好的总结。

这些同学有很多,我就不再一一点名了。感谢你们的同时,我也相信,积极参与并留言也会帮助你更深入地理解每一个知识点。所以我希望,你接下来可以多多留言给我,让我们一起学习,共同进步。

留言涉及的问题有很多,但我经过进一步地分析和总结后,发现针对分布式事务和分布式锁的问题比较多,同学们的疑惑也比较多。

确实,这两大问题属于分布式技术的关键问题。因此,今天的这篇答疑文章,我就围绕这两大问题来进行一次集中的分析和回答吧。

我们先来看一下分布式事务的相关问题。

分布式事务的相关问题

在第6篇文章“分布式事务:All or Nothing”中,我介绍了两阶段提交协议和三阶段提交协议。有很多同学提出了疑问:两阶段提交协议(2PC)和三阶段提交协议(3PC)的区别,到底是什么?

在回答这个问题前,我建议你先回到第6篇文章,去回忆一下它们的流程。然后,我们看看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篇文章“分布式选举:国不可一日无君”中的相关内容。

追问3:3PC也是只有一个协调者,为什么就不会有单点故障问题了?

首先,我先明确下这里所说的单点故障问题。

因为系统中只有一个协调者,那么协调者所在服务器出现故障时,系统肯定是无法正常运行的。所以说,2PC和3PC都会有单点故障问题。

但是,3PC因为在协调者和参与者中都引入了超时机制,可以减弱单点故障对整个系统造成的影响。为什么这么说呢?

因为引入的超时机制,参与者可以在长时间没有得到协调者响应的情况下,自动将超时的事务进行提交,不会像2PC那样被阻塞住。

好了,以上就是关于分布式事务中的2PC和3PC的相关问题了,相信你对这两个提交协议有了更深刻的认识。接下来,我们再看一下分布式锁的相关问题吧。

分布式锁的相关问题

在第7篇文章“分布式锁:关键重地,非请勿入”后的留言中,我看到很多同学都问到了分布式互斥和分布式锁的关系是什么。

我们先来回顾下,分布式互斥和分布式锁分别是什么吧。

在分布式系统中,某些资源(即临界资源)同一时刻只有一个程序能够访问,这种排他性的资源访问方式,就叫作分布式互斥。这里,你可以再回顾下第3篇文章中的相关内容。

分布式锁指的是,在分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。这里,你可以再回顾下第7篇文章中的相关内容。

分布式锁的目的是,保证多个进程访问临界资源时,同一时刻只有一个进程可以访问,以保证数据的正确性。因此,我们可以说分布式锁是实现分布式互斥的一种手段或方法。

除了分布式互斥和分布式锁的关系外,很多同学都针对基于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篇文章留言涉及的问题,进行了归纳总结,从中摘取了分布式事务和分布式锁这两个知识点,串成了今天这篇答疑文章。

今天没来得及和你扩展的问题,后续我会再找机会进行解答。最后,我要和你说的是,和我一起打卡分布式核心技术,一起遇见更优秀的自己吧。

篇幅所限,留言区见。

我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e5%88%86%e5%b8%83%e5%bc%8f%e6%8a%80%e6%9c%af%e5%8e%9f%e7%90%86%e4%b8%8e%e7%ae%97%e6%b3%95%e8%a7%a3%e6%9e%90/14%20%e7%ad%94%e7%96%91%e7%af%87%ef%bc%9a%e5%88%86%e5%b8%83%e5%bc%8f%e4%ba%8b%e5%8a%a1%e4%b8%8e%e5%88%86%e5%b8%83%e5%bc%8f%e9%94%81%e7%9b%b8%e5%85%b3%e9%97%ae%e9%a2%98.md