24、分布式系统并发:分布式锁(etcd/zookeeper)、Raft共识算法简介、CAP理论与并发、gRPC异步调用

好,咱们今天聊一个硬核话题——分布式系统里的并发。说实话,单机并发你已经玩得挺溜了,mutex、condition_variable、atomic 信手拈来。但一旦跨了进程、跨了机器,这些玩意儿全废了。为什么?因为共享内存没了。

分布式并发,核心问题就一个:怎么在没共享内存的情况下,让多个节点协调一致。我当年第一次接触分布式锁时,心想这不就是个远程 mutex 吗?后来被现实狠狠教育了一顿——网络延迟、节点宕机、脑裂,哪个都比死锁恶心十倍。

24.1 分布式锁:etcd vs ZooKeeper

分布式锁,说白了就是让多个进程互斥访问某个共享资源。但这里的「互斥」得靠外部协调服务来实现。目前主流的选择就两个:etcd 和 ZooKeeper。

24.1.1 基于 etcd 的分布式锁

etcd 用的是 Raft 协议(后面会讲),提供强一致性。它的锁实现思路很直接:利用 lease(租约)和 revision(版本号)。

我习惯用 etcd 的 v3 API 做锁,核心流程是这样的:

  1. 客户端创建一个 lease,比如 10 秒过期
  2. 在某个 key 上执行 put 操作,带上 lease ID
  3. 如果 put 成功,说明拿到锁了
  4. 如果 key 已存在,说明锁被占着,就 watch 这个 key
  5. 释放锁时直接 delete key,或者等 lease 过期自动释放

这里有个坑——租约续期。我曾经在生产环境遇到过,业务线程处理时间超过了 lease 的 TTL,结果锁自动释放了,另一个节点拿到锁开始写数据,两个节点同时操作同一份数据……嗯,那场面挺壮观的。解决方案是开一个后台协程定期续约,或者干脆把 TTL 设大一点,配合业务超时机制。

核心要点:etcd 锁的可靠性取决于 lease 续期机制。别把 TTL 设得太短,也别太长。我个人建议 TTL 设为业务最大处理时间的 2-3 倍。

24.1.2 基于 ZooKeeper 的分布式锁

ZooKeeper 的思路不太一样。它用的是临时顺序节点(Ephemeral Sequential Node)。

流程是这样的:

  1. 客户端在锁目录下创建临时顺序节点,比如 /lock/lock_0000001
  2. 获取目录下所有子节点,排序
  3. 如果自己是最小的那个,就拿到锁了
  4. 如果不是,就 watch 前一个节点
  5. 前一个节点被删除时,自己变成最小的,拿到锁

这个机制有个好处——公平锁。谁先来谁先得,不会出现 etcd 那种「抢锁」的竞争风暴。但缺点也很明显:ZooKeeper 的写性能不如 etcd,而且客户端会话超时会导致临时节点自动删除,锁就丢了。

特性 etcd ZooKeeper
一致性协议 Raft ZAB
锁实现方式 Lease + Revision 临时顺序节点
公平性 非公平 公平
性能 高(写 10k+ QPS) 中等(写 3k-5k QPS)
典型场景 Kubernetes、服务发现 Hadoop、Kafka 元数据

我的建议:如果你在云原生环境里,etcd 是首选,因为 Kubernetes 本身就依赖它。如果你在 Hadoop 生态里,ZooKeeper 更合适。别硬换,生态匹配很重要。

24.2 Raft 共识算法简介

Raft 是什么?说白了就是让一堆机器能达成一致的算法。你想想看,多个节点各自为政,怎么保证它们对同一个数据的认知是一样的?Raft 给出了一个工程上非常优雅的答案。

Raft 把节点分成三种角色:

  • Leader:负责处理所有写请求,把日志复制给 Follower
  • Follower:被动接收 Leader 的日志,投票选举 Leader
  • Candidate:选举时的临时角色

核心流程就两步:选举日志复制

选举时,Follower 如果超时没收到 Leader 的心跳,就变成 Candidate,给自己投票,然后请求其他节点投票。拿到多数票(N/2 + 1)就当选 Leader。

日志复制更简单:Leader 收到客户端请求,把日志追加到本地,然后并行发给所有 Follower。等多数 Follower 确认写入后,Leader 才提交这条日志,然后通知客户端成功。

我当年实现过一个简化版的 Raft 库,踩过最大的坑是选举超时时间。如果所有节点的超时时间一样,它们会同时变成 Candidate,导致选票分裂,谁也选不出来。解决方案是给每个节点一个随机化的超时时间,比如 150ms-300ms 之间随机。

注意:Raft 要求日志是连续的,不能有空洞。如果你在实现时发现日志索引跳过了,那一定是 bug。我曾经因为这个 bug 导致集群脑裂了两次,排查了整整两天。

24.3 CAP 理论与并发

CAP 理论,你肯定听过。C(一致性)、A(可用性)、P(分区容忍性),三者只能取其二。但我想说的是,这个理论在分布式并发场景下,有更具体的体现。

为什么?因为分区是必然的。网络会断,节点会挂,消息会丢。你没法避免 P,所以实际上你只能在 CP 和 AP 之间选。

  • CP 系统:比如 etcd、ZooKeeper。它们优先保证一致性,分区时可能拒绝写请求。适合存元数据、配置信息。
  • AP 系统:比如 Cassandra、DynamoDB。它们优先保证可用性,分区时允许写,但数据可能不一致。适合存用户会话、日志等。

在并发编程里,这个选择直接影响你的锁设计。举个例子:

如果你用 CP 的 etcd 做分布式锁,锁的可靠性很高,但网络分区时可能拿不到锁,业务就得等。如果你用 AP 的系统做锁,网络分区时两边都能拿到锁,那就出现「双主」了——两个节点都认为自己持有锁,同时写数据,数据就乱了。

我个人习惯是:分布式锁必须用 CP 系统。宁可拿不到锁,也不能拿错锁。数据一致性比可用性重要得多。

24.4 gRPC 异步调用

最后聊聊 gRPC 的异步调用。分布式系统里,节点之间通信是家常便饭。gRPC 基于 HTTP/2 和 Protobuf,性能好、跨语言,是微服务通信的首选。

但同步调用有个问题——阻塞。你发一个 RPC 请求,线程就卡在那等响应。如果对方处理慢,或者网络延迟高,线程池很快就满了。

异步调用就能解决这个问题。gRPC 提供了两种异步方式:

  • ClientAsyncResponseReader:客户端异步接收响应
  • ServerAsyncReaderWriter:服务端异步处理流式请求

我拿客户端异步调用举个例子:

// 创建 CompletionQueue
grpc::CompletionQueue cq;

// 创建异步 Stub
auto stub = MyService::NewStub(channel);

// 准备请求和响应
MyRequest request;
request.set_data("hello");
MyResponse response;
grpc::Status status;

// 创建异步调用上下文
grpc::ClientContext context;
std::unique_ptr<grpc::ClientAsyncResponseReader<MyResponse>> rpc(
    stub->PrepareAsyncMyMethod(&context, request, &cq));

// 发起异步调用
rpc->StartCall();

// 注册回调,等待完成
rpc->Finish(&response, &status, (void*)1);

// 从 CompletionQueue 中获取结果
void* got_tag;
bool ok;
cq.Next(&got_tag, &ok);

if (ok && got_tag == (void*)1) {
    // 处理响应
    std::cout << "Response: " << response.result() << std::endl;
}

这里有个关键点:CompletionQueue 必须由单独的线程驱动。你不能在主线程里阻塞等它,否则异步就变成同步了。我一般会开一个后台线程池,专门轮询 CompletionQueue,把回调分发到工作线程。

避坑指南:我曾经在异步调用里忘了检查 ok 标志。结果服务端返回错误时,我还在处理 response,直接崩溃了。记住:ok == false 表示 RPC 失败,response 内容不可用。

异步调用的好处是:一个线程可以同时管理成百上千个 RPC 调用。线程数不再受并发请求数限制,系统吞吐量大幅提升。但代价是代码复杂度增加了,你得自己管理回调状态和生命周期。

24.5 本章小结

分布式并发,核心就是解决「无共享内存下的协调问题」。分布式锁让你能互斥访问资源,Raft 让多个节点达成一致,CAP 理论帮你做架构取舍,gRPC 异步调用让你高效通信。

这些东西,每一个单独拿出来都能讲一整天。但今天我只挑最核心的讲——你真正写代码时用得上的那些。剩下的,留给你在实践中慢慢体会吧。

一句话总结:分布式并发没有银弹。etcd 锁、Raft 选举、CAP 取舍、gRPC 异步,都是工具。选对工具,比会用工具更重要。

分布式系统并发知识体系 分布式锁 Raft 共识算法 CAP 理论 gRPC 异步调用 etcd 锁 ZooKeeper 锁 Lease 续期 临时顺序节点 Leader 选举 日志复制 多数派提交 随机超时 一致性 (C) 可用性 (A) 分区容忍性 (P) CP vs AP 取舍 CompletionQueue 异步 Stub 回调管理 线程池驱动 核心思想:无共享内存下的协调 分布式锁 → 互斥访问 | Raft → 达成一致 | CAP → 架构取舍 | gRPC → 高效通信
公众号:蓝海资料掘金营,微信deep3321