假设有两个小孩想玩同一个玩具(临界资源),但玩具只有一个,必须保证一次只有一个人能够玩。当一个小孩在玩时,另一个小孩只能原地等待,直到玩完才能轮到自己。这就是 互斥(Mutual Exclusion)概念:任意时刻,只允许规定数量的进程(或节点)访问临界区,其余进程只能排队等待。
在单机(单台服务器)环境下,各进程共享同一台机器的内存和时钟,常用信号量、互斥锁等机制即可实现。但在分布式系统中,进程分散在不同的网络节点上,通信必须经过网络,面临以下几个挑战:
互联网特性
没有统一时钟
节点或网络故障
基于上述特性,分布式互斥算法应当满足:
常见的分布式互斥算法可分为三大类(本文重点介绍以下三种):
集中互斥算法(Centralized Algorithm)
基于许可的互斥算法(Permission-Based Algorithm)
核心思想:想要进入临界区的节点,需要向系统中其他节点请求许可(Permission),只有获得足够许可后才能访问。
代表算法:
令牌环互斥算法(Token Ring Algorithm)
下面分别详细讲解三种算法的工作原理、消息流程与优缺点。
系统中预先选举出一个节点作为 “协调者(Coordinator)”。
当某个进程(节点) Pi 想访问临界资源时,向协调者发送 REQUEST(Pi) 消息。
协调者维护一个本地的“请求队列(按先后次序)”。
当 Pi 使用完毕后,向协调者发送 RELEASE(Pi),协调者收到后,从请求队列中弹出下一个节点 Pj,并向其发送 GRANT(Pj),Pj 再进入临界区。
图 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) ──────→ 空队列
优点
缺点
当系统规模扩大时,如果依赖单一协调者,瓶颈和单点故障问题更严重。基于许可的互斥算法没有集中节点,而是由各个节点之间互相“投票”或“许可”来决定谁先进入临界区。典型代表有 Lamport 算法和 Richard & Agrawal 算法,它们都基于逻辑时钟与请求队列来保证先来后到与互斥。
逻辑时钟(Logical Clock)
Li = Li + 1
,并将新的 Li 作为时间戳 TSi 发送给其他节点。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 只有在以下两种条件同时满足时,才可进入临界区:
当 Pi 进入临界区并使用完毕后,向所有节点广播 RELEASE(Pi, TSi’)(TSi’ = Li + 1),各节点收到后:
Lj = max(Lj, TSi') + 1
;(Pi, …)
;这样,排在下一位的节点即可进入临界区。
图 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 排序,让下一位进入
消息数量:每次进入临界区需发送
3(n − 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 较小),则等待,并暂时不回复;REPLY(Pj, Lj)
,但 Pj 本地继续排队。将 (Pi, TSi)
插入本地请求队列,按时戳排序。
进入临界区的条件
REPLY
,并且在自己本地队列中排在最前;使用完成后
Pi 进入临界区使用完成后,将 (Pi, …)
从本地队列中删除,并向本地队列(如果有)最前面的下一个节点发送 “伪 REPLY”(即代替 RELEASE 的作用),让该节点尝试进入。
其余节点在收到 “伪 REPLY” 时,也会将 (Pi, …)
从自己的本地队列中删除,并检查本地队列头部,如果自己排在最前且已收到所有许可,就可进入临界区。
图 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) 从本地队列删除
若本地队列头为自己且已收到所有许可,则进入临界区。
消息开销
相比 Lamport 算法的 3(n − 1)
,Richard & Agrawal 算法每次只需 2(n − 1)
条消息:
REQUEST
给其他 n − 1 个节点;REPLY
即可进入;REPLY
(合并了原来的 RELEASE);优点
缺点
适用场景
只要令牌在系统内部一直循环传递,就能保证:
图 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)依此循环,令牌永远在环上移动,每个节点最终都能获得令牌。
优点
缺点
适用场景
算法名称 | 消息开销 | 互斥保证 | 公平性 | 单点故障 | 扩展性 | 适用场景 |
---|---|---|---|---|---|---|
集中互斥算法 | 3 条(REQUEST→GRANT→RELEASE) | 绝对互斥 | 先来后到 | 存在单点故障 | 较差 | 小规模、对延迟要求不高的场景 |
Lamport 算法 | 3(n − 1) 条 | 无单点故障 | 严格基于时间戳 | 无单点故障 | 中等 | 节点数不多、资源请求较频繁的场景 |
Richard & Agrawal 算法 | 2(n − 1) 条 | 无单点故障 | 基于时间戳 | 无单点故障 | 中等 | 想在 Lamport 基础上减少消息量的场景 |
令牌环互斥算法 | 每次令牌传递 1 条 → 平均 n/2 条 | 无单点故障 (若令牌丢失则宕机) | 轮询公平 | 令牌丢失视为故障 | 较差 (环受拓扑限制) | 小规模、节点变动少、资源访问频繁且快的场景 |
系统规模很小(节点数 ≤ 10)且对延迟要求不高
系统规模中等(节点数约 10 – 50),去中心化,又希望保证较高公平性
系统规模小且临界资源访问速度快、频率高
系统需要高度容错、节点可能频繁增删
不同场景应根据节点规模、资源访问频率、容错需求等综合考量,选择最合适的分布式互斥算法。以上内容配以示意图,力求图文并茂、层次清晰,帮助您快速理解三类算法的工作原理和应用场景。