专栏文章
专栏文章
分布式系统系列
1. 分布式系统 #01:分布式一致性 2. 分布式系统 #02:分布式事务 3. 分布式系统 #03:分布式协调与选举 4. 分布式系统 #04:分布式系统设计模式 5. 分布式系统 #05:金融级分布式系统实践

分布式系统 #03:分布式协调与选举

发布于 2026-05-28 14:04 👁 17 次阅读
#分布式#coordination

本文系统梳理分布式系统中的协调与选举技术:分布式锁的三种方案对比、分布式 ID 的生成策略、一致性哈希的原理与虚拟节点、服务注册与发现的选型,以及 Bully/ZAB 等 Leader 选举算法。读完能在工程中做出有依据的方案选择。

相关文章分布式一致性 · 分布式事务 · 分布式系统设计模式 · 金融级分布式系统实践


目录

章节 说明
分布式锁 数据库/Redis/ZooKeeper 三种方案对比
分布式 ID UUID/雪花算法/号段模式各方案优劣
一致性哈希 虚拟节点解决数据倾斜
服务注册与发现 Eureka/Nacos/etcd 对比
Leader 选举 Bully/Raft/ZAB 算法

分布式锁

分布式锁是在分布式环境下,实现多进程对临界资源互斥访问的标记机制。

为什么需要分布式锁

单机的线程锁(synchronized、ReentrantLock)只能管控同一进程内的并发,无法管控跨机器的并发访问。

典型场景:电商库存扣减。用户 A 和用户 B 同时读到库存为 2,A 买走 1 个后库存变为 1,但 B 的请求基于旧的库存数据执行,导致超卖。

三种实现方案

方案一:基于数据库

原理:创建一张锁表,利用数据库唯一性约束实现互斥。获取锁 = 插入记录,释放锁 = 删除记录。

-- 锁表
CREATE TABLE distributed_lock (
    resource_name VARCHAR(64) NOT NULL UNIQUE,  -- 资源名(唯一约束)
    holder        VARCHAR(64) NOT NULL,          -- 锁持有者
    created_at    DATETIME    NOT NULL
);

-- 获取锁(成功则持有,失败则等待)
INSERT INTO distributed_lock(resource_name, holder, created_at)
VALUES ('inventory:sku001', 'server-1', NOW());

-- 释放锁
DELETE FROM distributed_lock WHERE resource_name = 'inventory:sku001';

问题

方案二:基于 Redis(推荐高并发场景)

原理:利用 Redis 的 SET key value NX PX 命令实现原子性的加锁,value 为唯一标识(防止误删他人的锁)。

# 获取锁:key 不存在时设置,并设置过期时间(防死锁)
SET lock:inventory:sku001 {uuid} NX PX 30000

# 释放锁:Lua 脚本保证原子性(先验证是自己的锁,再删除)
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

优点

缺点

方案三:基于 ZooKeeper(推荐高可靠场景)

原理:利用 ZooKeeper 的临时顺序节点实现分布式锁。

/lock/
  ├── lock-0000000001  ← 编号最小,持有锁
  ├── lock-0000000002  ← 监听前一个节点
  └── lock-0000000003  ← 监听前一个节点

流程

  1. /lock/ 下创建临时顺序节点
  2. 获取所有子节点,判断自己的编号是否最小
  3. 若最小 → 获得锁;若不最小 → 监听前一个节点的删除事件
  4. 前一个节点删除(持有者释放锁或宕机)→ 收到通知 → 重新判断

优点

缺点

羊群效应:若监听父节点(所有子节点),锁释放时所有等待者同时被唤醒,大量无效通知。解决方案:每个节点只监听前一个节点,而非所有节点。

三种方案对比

维度 数据库 Redis ZooKeeper
性能
可靠性 中(有单点问题) 中(有锁丢失风险)
实现复杂度 低(有封装框架)
死锁风险 有(无自动过期) 低(有过期时间) 无(临时节点)
适用场景 并发低,对性能要求低 高并发,允许极低概率锁丢失 高可靠,对性能要求不极端

生产推荐:高并发场景选 Redis(注意 Redlock 或 Lua 脚本保证原子性);对可靠性要求高的场景选 ZooKeeper(Curator 框架封装完善)。


分布式 ID

分布式系统中,多个节点同时生成 ID,需要保证全局唯一性,同时满足有序性、高性能等要求。

方案对比

方案 原理 有序性 性能 依赖 典型问题
UUID 128 位随机数 无序 极高(本地生成) 字符串,索引性能差;无序导致 B+ 树频繁分裂
数据库自增 数据库 AUTO_INCREMENT 有序 低(依赖数据库) 数据库 单点瓶颈,分库分表后需特殊处理
雪花算法(Snowflake) 时间戳 + 机器 ID + 序列号 趋势递增 极高(本地生成) 时钟 时钟回拨导致 ID 重复
号段模式 批量从数据库取号段,本地分配 趋势递增 数据库 服务重启浪费号段
Redis INCR 利用 Redis 原子自增 有序 Redis Redis 持久化策略影响可靠性

雪花算法(Snowflake)详解

Twitter 开源的分布式 ID 生成算法,生成 64 位整数:

| 符号位(1bit) | 时间戳(41bit) | 机器ID(10bit) | 序列号(12bit) |
|      0        |  毫秒级时间戳   |  最多1024台机器 |  每毫秒4096个  |

时钟回拨问题

号段模式详解

数据库记录:
| biz_tag | max_id | step | update_time |
| order   | 1000   | 1000 | ...         |

本地缓存号段:[1, 1000],用完后再取 [1001, 2000]

一致性哈希

一致性哈希解决了普通哈希在节点增减时大量数据迁移的问题,是分布式存储中数据路由的核心算法。

普通哈希的问题

hash(key) % N:当节点数 N 变化时,几乎所有 key 的映射都会改变。

3 节点 → 4 节点:需迁移约 75% 的数据
10 节点 → 11 节点:需迁移约 91% 的数据

一致性哈希原理

将整个哈希值空间(0 ~ 2^32-1)组织成一个虚拟圆环(哈希环)

flowchart LR
    subgraph 哈希环
        direction TB
        A["节点A\n(hash=100)"] --> B["节点B\n(hash=200)"]
        B --> C["节点C\n(hash=300)"]
        C --> A
    end
    k1["key-01\n(hash=150)"] -->|顺时针找到第一个节点| B
    k2["key-02\n(hash=250)"] -->|顺时针找到第一个节点| C
    k3["key-03\n(hash=50)"] -->|顺时针找到第一个节点| A

寻址规则:对 key 计算哈希值,在哈希环上顺时针找到第一个节点,该节点负责存储此 key。

节点变化影响范围

3 节点 → 4 节点:只需迁移约 25% 的数据(理论上 1/N)
10 节点 → 11 节点:只需迁移约 9% 的数据

虚拟节点解决数据倾斜

当节点数量少时,节点在哈希环上分布不均匀,导致数据访问冷热不均(大部分请求集中在少数节点)。

解决方案:为每个物理节点创建多个虚拟节点,分散在哈希环上。

物理节点 A → 虚拟节点 A-01, A-02, A-03
物理节点 B → 虚拟节点 B-01, B-02, B-03
物理节点 C → 虚拟节点 C-01, C-02, C-03

虚拟节点数量越多,数据分布越均匀。实践中每个物理节点通常对应 100~200 个虚拟节点。

适用场景

注意:一致性哈希适合简单的路由寻址场景,不需要维护路由信息。但不适合需要范围查询的场景(哈希后数据无序)。


服务注册与发现

服务注册与发现解决微服务架构中"服务 A 如何找到服务 B 的 IP 和 Port"的问题。

核心问题

  1. 统一的中介存储:调用方在唯一的地方获取被调用服务的所有实例信息
  2. 状态更新与通知:服务实例的信息能够及时更新并通知到服务调用方

工作原理

sequenceDiagram
    participant S as 服务实例(被调用方)
    participant R as 注册中心
    participant C as 调用方
    Note over S,R: 服务注册(心跳续约)
    S->>R: 注册:IP=10.0.0.1, Port=8080, TTL=90s
    loop 每30秒
        S->>R: 心跳续约
    end
    Note over C,R: 服务发现(订阅变更)
    C->>R: 订阅服务B的实例列表
    R-->>C: 返回当前实例列表
    Note over R,C: 实例状态变更时通知
    R->>C: 推送变更(实例上线/下线)
    Note over S: 实例崩溃,停止心跳
    Note over R: 90秒后自动清除该实例
    R->>C: 推送变更(实例下线)

主流方案对比

维度 Eureka(Netflix) Nacos(阿里) etcd(CoreOS) ZooKeeper
CAP 模型 AP AP + CP(可切换) CP CP
一致性协议 无(最终一致) Raft(CP 模式) Raft ZAB
健康检查 客户端心跳 客户端心跳 + 服务端探测 Lease 机制 临时节点
配置管理 不支持 支持 支持 支持
服务网格 不支持 支持 支持(Kubernetes) 不支持
适用场景 高可用优先,容忍短暂不一致 国内微服务首选 Kubernetes 生态 老系统,需强一致

AP vs CP 的选择

推荐选 AP(如 Eureka、Nacos AP 模式)

服务发现是整个分布式系统的基石,可用性是最关键的设计目标。原因:

类比:整个互联网的服务发现系统就是 DNS,它是一个典型的 AP 系统(DNS 记录更新有延迟,但保证高可用)。


Leader 选举

分布式集群需要一个主节点(Leader)来协调和管理其他节点,保证集群有序运行和数据一致性。

dist raft election

dist raft log

Bully 算法(霸道总裁)

原则:"长者为大",ID 最大的活跃节点成为 Leader。

流程

  1. 检测到 Leader 宕机,发起选举
  2. 向所有 ID 比自己大的节点发送 Election 消息
  3. 若在超时时间内未收到 Alive 响应 → 自己成为 Leader,广播 Victory 消息
  4. 若收到 Alive 响应 → 等待对方发送 Victory 消息
优点 缺点
简单,选举速度快 需要全局节点信息
易于实现 新节点加入或节点恢复都会触发重新选举,可能频繁切主

典型应用:MongoDB 副本集故障转移(以最后操作时间戳作为 ID)。

Raft 选举(民主投票)

10 分布式一致性 - Raft 协议 章节。核心:随机超时 + 大多数票。

优点 缺点
选举稳定,不会轻易切主 需要节点间互相通信,通信量大
正确性有严格证明 需要获得过半投票

ZAB 选举(有优先级的民主投票)

ZooKeeper Atomic Broadcast 的选举算法,在 Raft 基础上增加了数据新鲜度的考量。

选举原则server_zxID(数据 ID)最大者优先;相同则 server_id 最大者当选。

节点状态:Looking(选举中)→ Leading(主节点)/ Following(跟随者)/ Observing(观察者,无投票权)

三阶段流程

sequenceDiagram
    participant S1 as Server-1
    participant S2 as Server-2
    participant S3 as Server-3
    Note over S1,S3: 初始状态,所有节点 Looking,各推选自己
    S1->>S2: 投票 <epoch=1, vote_id=1, zxID=0>
    S1->>S3: 投票 <epoch=1, vote_id=1, zxID=0>
    S2->>S1: 投票 <epoch=1, vote_id=2, zxID=0>
    S2->>S3: 投票 <epoch=1, vote_id=2, zxID=0>
    S3->>S1: 投票 <epoch=1, vote_id=3, zxID=0>
    S3->>S2: 投票 <epoch=1, vote_id=3, zxID=0>
    Note over S1,S3: 比较:zxID 相同,server_id 最大为 3
    S1->>S2: 更新投票 → vote_id=3
    S2->>S1: 更新投票 → vote_id=3
    Note over S3: 收到多数票,成为 Leader
    S3->>S1: Victory(我是 Leader)
    S3->>S2: Victory(我是 Leader)
优点 缺点
数据最新的节点优先当选,减少数据丢失 需要知道所有节点的 ID 和数据 ID
选举稳定性好 广播风暴(n*(n-1) 条消息)

三种选举算法对比

维度 Bully Raft ZAB
选举原则 ID 最大 多数票 数据最新 + 多数票
消息复杂度 O(n²) O(n) O(n²)
切主稳定性 低(新节点就触发)
数据保证 日志完整性 数据最新性
典型应用 MongoDB etcd、Consul ZooKeeper

为什么选奇数节点:多数派算法需要超过半数节点投票。偶数节点时,两个候选人各获一半票会导致选举失败,需重新投票。奇数节点(3、5、7)避免了这种平票情况,且容错能力相同(3 节点和 4 节点都只能容忍 1 个节点故障)。


参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论