<audio id="audio" title="14 | 答疑篇：分布式事务与分布式锁相关问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/0e/a27f98e580e56d6f2fbf48667fb7cc0e.mp3"></audio>

你好，我是聂鹏程。

到目前为止，“分布式技术原理与算法解析”专栏已经更新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)”中的相关内容。

**追问3：3PC也是只有一个协调者，为什么就不会有单点故障问题了？**

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

因为系统中只有一个协调者，那么协调者所在服务器出现故障时，系统肯定是无法正常运行的。所以说，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篇文章留言涉及的问题，进行了归纳总结，从中摘取了分布式事务和分布式锁这两个知识点，串成了今天这篇答疑文章。

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

篇幅所限，留言区见。

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