分布式互斥算法
1. 概述:什么是分布式互斥
假设有两个小孩想玩同一个玩具(临界资源),但玩具只有一个,必须保证一次只有一个人能够玩。当一个小孩在玩时,另一个小孩只能原地等待,直到玩完才能轮到自己。这就是 互斥(Mutual Exclusion)概念:任意时刻,只允许规定数量的进程(或节点)访问临界区,其余进程只能排队等待。
- 临界资源:在例子中即为玩具;在分布式系统中可能是某个文件、数据库记录、硬件设备等。
- 竞态:多个进程同时争夺同一个资源时发生竞争。
- 互斥:每次只允许一个进程进入临界区,其他进程只能先等待。
在单机(单台服务器)环境下,各进程共享同一台机器的内存和时钟,常用信号量、互斥锁等机制即可实现。但在分布式系统中,进程分散在不同的网络节点上,通信必须经过网络,面临以下几个挑战:
-
互联网特性
- 不同节点通过网络传输消息,会有网络延迟、丢包或乱序等不确定性。
- 节点彼此没有共享内存,只能依赖消息传递来协调对临界资源的访问。
-
没有统一时钟
- 各节点各自拥有物理时钟,难以做到完全同步。
- 如果无法知道谁先谁后,就无法保证公平先来后到,需要引入逻辑时钟或其他机制来解决。
-
节点或网络故障
- 某个节点或链路出现故障时,如何检测并进行故障恢复,才能保证系统依旧可用且不会长时间阻塞。
基于上述特性,分布式互斥算法应当满足:
- 互斥性:任何时刻最多只有一个节点能够进入临界区。
- 无饥饿(无饿死):每个请求都能在有限时间内得到满足,不会无限期等待。
- 无死锁:避免多个节点相互等待对方持有的资源,从而导致整体停滞。
- 公平性:先发出请求的节点应当先获得访问权,或至少不会被长期“插队”。
2. 分布式互斥算法分类
常见的分布式互斥算法可分为三大类(本文重点介绍以下三种):
-
集中互斥算法(Centralized Algorithm)
- 核心思想:引入一个全局 “协调者(Coordinator)”,所有节点的访问请求都交由它来排序和授权。
-
基于许可的互斥算法(Permission-Based Algorithm)
-
核心思想:想要进入临界区的节点,需要向系统中其他节点请求许可(Permission),只有获得足够许可后才能访问。
-
代表算法:
- Lamport 算法
- Richard & Agrawal 算法
-
-
令牌环互斥算法(Token Ring Algorithm)
- 核心思想:在所有节点之间形成一个逻辑环,只有持有 “令牌(Token)” 的节点才能访问临界区,令牌在环上顺序传递。
下面分别详细讲解三种算法的工作原理、消息流程与优缺点。
3. 集中互斥算法
3.1 核心思路
-
系统中预先选举出一个节点作为 “协调者(Coordinator)”。
-
当某个进程(节点) Pi 想访问临界资源时,向协调者发送 REQUEST(Pi) 消息。
-
协调者维护一个本地的“请求队列(按先后次序)”。
- 若此时没有其他节点在使用资源,则直接向 Pi 发送 GRANT(Pi),Pi 收到后即可进入临界区。
- 若已有其他节点持有该资源,则把 Pi 的请求追加到队列尾,等待前面的节点释放后再处理。
-
当 Pi 使用完毕后,向协调者发送 RELEASE(Pi),协调者收到后,从请求队列中弹出下一个节点 Pj,并向其发送 GRANT(Pj),Pj 再进入临界区。
3.2 关键特性
- 互斥保证:协调者一次只能向一个节点发放 GRANT,确保只有一个节点能够访问。
- 先来后到:协调者将所有请求排队,根据请求到达时间顺序依次授权。
- 避免饥饿:如果某节点长时间占用或宕机,协调者可检测超时并将其移出队列(或让其“自动释放”),从而让后续请求有机会执行。
3.3 消息交互流程(以两个节点 P1、P2 为例,详见图 3-1)
图 3-1:集中互斥算法消息流程示意P1 协调者 P2
(1) REQUEST(P1) ──────→ 存入队列:[(P1, TS1)]
(2) GRANT(P1) ←────── ←────── REQUEST(P2)更新队列:[(P1, TS1), (P2, TS2)]
(3) P1 进入临界区
(4) RELEASE(P1) ──────→ 更新队列:[(P2, TS2)]
(5) GRANT(P2) ←──────
(6) P2 进入临界区
(7) RELEASE(P2) ──────→ 空队列
- 第 1 步:P1 向协调者发送 REQUEST(P1),时间戳记为 TS1。协调者将 (P1, TS1) 加入本地队列。
- 第 2 步:发现队列仅有 P1,直接向 P1 返回 GRANT(P1)。此时 P1 进入临界区。
- 第 3 步:P2 请求到达,发送 REQUEST(P2),时间戳记为 TS2(TS2 > TS1)。协调者将 (P2, TS2) 加入队列尾。
- 第 4 步:P1 使用完成后,发送 RELEASE(P1)。协调者将 P1 从队列中移除,只剩 (P2, TS2),于是向 P2 发放 GRANT(P2)。
- 第 5 步:P2 收到 GRANT 进入临界区,使用完后再通知 RELEASE(P2),队列空。
3.4 优缺点分析
-
优点
- 实现简单,理解直观;
- 每次临界区访问仅需 3 次消息:REQUEST → GRANT → RELEASE。
-
缺点
- 单点故障:协调者若发生故障,整个系统无法继续;
- 扩展性差:协调者会成为性能瓶颈,随着节点数增多,排队和消息处理压力变大;
- 协调者可能出现宕机或网络隔离,需要额外的故障检测与选举机制。
4. 基于许可的互斥算法
当系统规模扩大时,如果依赖单一协调者,瓶颈和单点故障问题更严重。基于许可的互斥算法没有集中节点,而是由各个节点之间互相“投票”或“许可”来决定谁先进入临界区。典型代表有 Lamport 算法和 Richard & Agrawal 算法,它们都基于逻辑时钟与请求队列来保证先来后到与互斥。
4.1 Lamport 算法
4.1.1 核心思想
-
逻辑时钟(Logical Clock)
- 每个节点 Pi 维护一个本地逻辑时钟 Li,初始值为 0。
- 当 Pi 发出临界区请求时,先执行
Li = Li + 1
,并将新的 Li 作为时间戳 TSi 发送给其他节点。 - 当 Pj 接收到一条请求消息(或 RELEASE)时,先将本地时钟更新为
Lj = max(Lj, TSi) + 1
,再处理消息。
-
请求队列(Request Queue)
-
每个节点维护一个本地请求队列,队列中元素为
(节点ID, 时间戳)
,按时间戳从小到大排序。如时间戳相等,则按照节点 ID 从小到大排序。 -
当 Pi 发出 REQUEST(Pi, TSi) 时,会将自己这一请求加入本地队列,并向其它所有节点广播 REQUEST(Pi, TSi)。
-
当 Pj(j ≠ i)收到 REQUEST(Pi, TSi) 后:
- 更新本地逻辑时钟:
Lj = max(Lj, TSi) + 1
; - 将
(Pi, TSi)
插入本地请求队列; - 向 Pi 发送一条 REPLY(Pj, Lj)(表明已许可)。
- 更新本地逻辑时钟:
-
-
进入临界区的条件
-
Pi 只有在以下两种条件同时满足时,才可进入临界区:
- 本地队列中排在最前(时间戳最小且 ID 最小);
- 已收到来自所有其他节点的 REPLY 消息。
-
当 Pi 进入临界区并使用完毕后,向所有节点广播 RELEASE(Pi, TSi’)(TSi’ = Li + 1),各节点收到后:
- 更新本地逻辑时钟:
Lj = max(Lj, TSi') + 1
; - 从本地请求队列中删除
(Pi, …)
;
- 更新本地逻辑时钟:
这样,排在下一位的节点即可进入临界区。
-
4.1.2 消息流程示意(图 4-1)
图 4-1:Lamport 算法消息交互(3 个节点 P1、P2、P3)P1 P2 P3
(1)L1= L1+1;TS1=1请求:REQUEST(P1,1) ──→更新 L2=max(L2,1)+1=2本地队列插入 (P1,1)←── REPLY(P2,2)更新 L3=max(L3,1)+1=2本地队列插入 (P1,1)←── REPLY(P3,2)(2)P1 收齐 P2、P3 的 REPLY,且自己在 3 个节点队列中排第一→ 进入临界区(3)使用完毕:L1 = L1 + 1 = 2;广播 RELEASE(P1,2)P2、P3 更新时钟并删除 (P1,1)(4)此时本地队列中可能有 (P2,2)、(P3,2) 等请求根据时间戳和节点 ID 排序,让下一位进入
-
消息数量:每次进入临界区需发送
- n – 1 条 REQUEST(广播给其它节点),
- n – 1 条 REPLY(每个节点一个),
- n – 1 条 RELEASE(广播)。
共计3(n − 1)
条消息。
4.1.3 特点与适用场景
-
优点
- 无集中节点,无单点故障;
- 基于时间戳排序,保证先到先得,具有公平性;
- 不存在死锁或循环等待。
-
缺点
- 通信开销较大:消息数随节点数线性增长,适合节点规模较小、临界资源请求频率相对较低的场景;
- 每个节点需维护本地请求队列,存储开销为 O(n)。
4.2 Richard & Agrawal 算法
4.2.1 算法思路
Richard & Agrawal 算法是在 Lamport 算法基础上的改进,目的是减少消息开销,将原本的 RELEASE 与 REPLY 合并,以降低通信量。主要思路如下:
-
发送请求
-
当 Pi 想进入临界区时:
- 将本地逻辑时钟
Li = Li + 1
,得到时间戳 TSi; - 将
(Pi, TSi)
插入本地请求队列; - 向其它所有节点广播
REQUEST(Pi, TSi)
。
- 将本地逻辑时钟
-
-
接收请求并判断
-
Pj(j ≠ i)收到
REQUEST(Pi, TSi)
后:-
更新本地逻辑时钟
Lj = max(Lj, TSi) + 1
; -
若 Pj 当前 没有正在等待或占用临界资源,则立即给 Pi 回复
REPLY(Pj, Lj)
; -
若 Pj 正在等待或访问临界资源,则比较
(TSj, Pj)
与(TSi, Pi)
:- 若
(TSi, Pi)
排在前面(时间戳更小或时间戳相同且 ID 较小),则等待,并暂时不回复; - 否则(即 Pj 的请求优先级更高),向 Pi 立刻回复
REPLY(Pj, Lj)
,但 Pj 本地继续排队。
- 若
-
将
(Pi, TSi)
插入本地请求队列,按时戳排序。
-
-
-
进入临界区的条件
- Pi 必须从所有 n – 1 个节点都收到
REPLY
,并且在自己本地队列中排在最前; - 方能进入临界区。
- Pi 必须从所有 n – 1 个节点都收到
-
使用完成后
-
Pi 进入临界区使用完成后,将
(Pi, …)
从本地队列中删除,并向本地队列(如果有)最前面的下一个节点发送 “伪 REPLY”(即代替 RELEASE 的作用),让该节点尝试进入。 -
其余节点在收到 “伪 REPLY” 时,也会将
(Pi, …)
从自己的本地队列中删除,并检查本地队列头部,如果自己排在最前且已收到所有许可,就可进入临界区。
-
4.2.2 消息流程示意(图 4-2)
图 4-2:Richard & Agrawal 算法消息流程(3 个节点 P1、P2、P3)P1 P2 P3
(1)L1 = L1 + 1;TS1 = 1请求:REQUEST(P1,1) ──→ 更新 L2 = max(L2,1)+1 = 2本地队列插入 (P1,1)P2 无本地请求 → 立即 REPLY(P2,2)←──更新 L3 = max(L3,1)+1 = 2本地队列插入 (P1,1)P3 无本地请求 → 立即 REPLY(P3,2)←──(2)P1 收到 P2、P3 的 REPLY,且本地队列头为 (P1,1)→ 进入临界区(3)使用完毕:删除本地队列中 (P1,1),向本地队列中后续进程 “发送 REPLY” —— 假设本地队列后续为 (P2,2),则发送 REPLY(P2, …),允许 P2 进入。(4)其余节点收到 P1 的删除信号后,也将 (P1,1) 从本地队列删除 若本地队列头为自己且已收到所有许可,则进入临界区。
4.2.3 特点与对比
-
消息开销
-
相比 Lamport 算法的
3(n − 1)
,Richard & Agrawal 算法每次只需2(n − 1)
条消息:- 一轮
REQUEST
给其他 n − 1 个节点; - 收到的 n − 1 条
REPLY
即可进入; - 使用完成后,节点会“顺便”触发对下一个队列头的
REPLY
(合并了原来的 RELEASE);
- 一轮
-
-
优点
- 与 Lamport 算法相比,消息量减少,通信效率更高;
- 依旧使用时间戳排序,保证先来后到,公平性较好;
-
缺点
- 相对于集中式或令牌环算法,存储开销仍为 O(n);
- 需要维护本地请求队列、逻辑时钟,算法逻辑略微复杂;
-
适用场景
- 中等规模(节点数不太大)且资源竞争不太频繁的场景;
- 希望在去中心化的同时尽量减少通信开销。
5. 令牌环互斥算法
5.1 核心思路
- 系统中将所有节点按某种顺序构成一个逻辑环(Ring)。
- 整个系统中仅保有一个 “令牌(Token)”,它代表了访问临界区的唯一许可。
- 只有持有令牌的节点才能进入临界区,使用完毕后必须将令牌传给环上的下一个节点。
- 若节点收到令牌但不需要访问临界区,则直接将令牌传给下一个。
只要令牌在系统内部一直循环传递,就能保证:
- 互斥性:同时只有一个令牌,自然只有一个节点能进入临界区;
- 公平性:令牌按固定顺序依次传递,每个节点都能轮到;
- 消息开销可控:令牌在环上每传递一次,记为 1 条消息;若环上有 n 个节点,则令牌完整绕行一圈需 n 条消息,平均到每个请求上的消息开销约为 n/2。
5.2 消息流程示意(图 5-1)
图 5-1:5 个节点构成的令牌环示意P1 → P2 → P3 → P4 → P5 → P1(循环)(1)假设初始令牌在 P1,若 P1 需要访问: P1 收到令牌 → 进入临界区 → 使用完毕 → 发送 TOKEN → P2 (2)若 P2 在收到令牌时不需要访问,则直接 “转发” 令牌: P2 将令牌发送给 P3 → … (3)若 P3 需要访问,则: P3 收到令牌 → 进入临界区 → 使用完毕 → 发送 TOKEN → P4(4)依此循环,令牌永远在环上移动,每个节点最终都能获得令牌。
- 消息数:每次令牌传递算作 1 条消息。若某个节点从请求到实际获得令牌需要等待 k 步,则消息数为 k (直到令牌传到为止)。
5.3 特点与缺点
-
优点
- 无需广播,仅在相邻两节点之间传递令牌,通信开销较小;
- 保证了先来后到的“轮询”机制,公平性好;
- 不存在死锁:只要令牌不丢失,节点始终能轮到。
-
缺点
- 令牌丢失风险:若持有令牌的节点宕机或网络隔离,令牌就会丢失,整个系统无法访问临界区,需额外机制检测并重建令牌;
- 环重构复杂:节点加入或退出都会破坏原有环,需要重新组织环拓扑,且重构过程中无法使用临界资源;
- 延迟受环大小影响:若环上节点数较多,则令牌传递到某一节点的延迟线性增大,不适合大规模场景;
-
适用场景
- 节点数量有限(小规模系统),资源访问频率高且使用时间短;
- 网络相对可靠、节点变动较少的场景。
6. 三种算法对比小结
算法名称 | 消息开销 | 互斥保证 | 公平性 | 单点故障 | 扩展性 | 适用场景 |
---|---|---|---|---|---|---|
集中互斥算法 | 3 条(REQUEST→GRANT→RELEASE) | 绝对互斥 | 先来后到 | 存在单点故障 | 较差 | 小规模、对延迟要求不高的场景 |
Lamport 算法 | 3(n − 1) 条 | 无单点故障 | 严格基于时间戳 | 无单点故障 | 中等 | 节点数不多、资源请求较频繁的场景 |
Richard & Agrawal 算法 | 2(n − 1) 条 | 无单点故障 | 基于时间戳 | 无单点故障 | 中等 | 想在 Lamport 基础上减少消息量的场景 |
令牌环互斥算法 | 每次令牌传递 1 条 → 平均 n/2 条 | 无单点故障 (若令牌丢失则宕机) | 轮询公平 | 令牌丢失视为故障 | 较差 (环受拓扑限制) | 小规模、节点变动少、资源访问频繁且快的场景 |
6.1 选型建议
-
系统规模很小(节点数 ≤ 10)且对延迟要求不高
- 可以考虑集中互斥算法:实现最简单,但要保证协调者高可用;
-
系统规模中等(节点数约 10 – 50),去中心化,又希望保证较高公平性
- 可优先选择 Richard & Agrawal 算法,由于消息量比 Lamport 算法少,且同样保证先来后到;
-
系统规模小且临界资源访问速度快、频率高
- 令牌环互斥适合:令牌在环上快速循环,通信开销低,但必须保障令牌不会丢失,环重构开销也要评估;
-
系统需要高度容错、节点可能频繁增删
- Lamport 算法更易于动态扩展,不用维护环结构,但通信成本更高;
7. 总结
- 分布式互斥算法的核心目标:在无共享内存、无全局时钟的分布式环境下,通过消息传递保证临界资源的互斥访问,避免并发冲突、死锁与饥饿。
- 集中互斥算法:思想最简单、通信量最少,但存在单点故障和可扩展性差的问题;
- 基于许可的互斥算法(Lamport、Richard & Agrawal):去中心化、没有单点故障,采用逻辑时钟保证先来后到,通信开销随节点数线性增加;
- 令牌环互斥算法:令牌唯一、通信开销低,但令牌丢失或环结构变更的容错难度大。
不同场景应根据节点规模、资源访问频率、容错需求等综合考量,选择最合适的分布式互斥算法。以上内容配以示意图,力求图文并茂、层次清晰,帮助您快速理解三类算法的工作原理和应用场景。