一、分布式系统基础:CAP理论、一致性模型、Raft协议简介、分布式锁、服务发现
说实话,分布式系统这个话题,我当年刚接触时也觉得头大。你想想看,一台机器干活好好的,为什么要搞那么多台?但等你真正面对高并发、海量数据时,就会发现——单机再强也有天花板。今天这一章,咱们就把分布式系统里最核心的几个概念掰开揉碎讲清楚。
1.1 CAP理论:分布式系统的“不可能三角”
CAP理论是分布式系统的基石。它告诉我们:一个分布式系统,最多只能同时满足以下三个特性中的两个:
- C(Consistency)一致性:所有节点在同一时刻看到的数据是一样的。
- A(Availability)可用性:每个请求都能得到一个非错误的响应。
- P(Partition Tolerance)分区容错性:系统允许网络分区(节点间通信中断)的存在。
为什么说这是“不可能三角”?我举个例子你就明白了。假设你有两个节点A和B,网络突然断了。这时候:
- 如果你选择CP(一致+分区容忍),那么A和B必须保持数据一致,但网络断了没法同步,所以只能拒绝写请求——可用性就没了。
- 如果你选择AP(可用+分区容忍),那么A和B各自接受写请求,数据就不一致了。
- 如果你选择CA(一致+可用),那网络分区时系统直接崩溃——这在分布式系统中几乎不可接受。
核心结论:在分布式系统中,P(分区容错)是必须选的。你真正要权衡的是C和A。比如ZooKeeper偏CP,Eureka偏AP。
我在项目中遇到过一个问题:早期我们用了强一致性的方案,结果网络抖动时整个服务不可用,用户疯狂投诉。后来改成最终一致性+补偿机制,虽然数据短暂不一致,但系统可用性大幅提升。嗯,这里要注意——没有银弹,只有取舍。
1.2 一致性模型:从强到弱
一致性模型说白了就是“数据同步的松紧程度”。我习惯把它们分成三类:
| 模型 | 特点 | 典型场景 |
|---|---|---|
| 强一致性 | 写完后,所有读都能看到最新数据 | 银行转账、库存扣减 |
| 最终一致性 | 数据会在一段时间后达成一致 | DNS解析、社交动态 |
| 弱一致性 | 不保证后续读能看到写的结果 | 实时日志、监控指标 |
你想想看,强一致性听起来很完美,但代价是什么?性能下降、可用性降低。我做过一个秒杀系统,最开始用强一致性,结果QPS上不去。后来改成最终一致性+异步对账,性能直接翻了三倍。
我的建议:能用最终一致性解决的问题,就别上强一致性。系统越简单,越不容易出幺蛾子。
1.3 Raft协议简介:分布式共识的“白话版”
Raft协议是分布式共识算法里最容易理解的一个。它把复杂的共识问题拆成了三个子问题:
- Leader选举:选出一个老大来协调写操作。
- 日志复制:Leader把写操作广播给所有Follower。
- 安全性:保证只有拥有最新日志的节点才能成为Leader。
Raft的核心思想其实很简单——多数派同意。只要超过一半的节点确认了写操作,这个操作就算成功了。
我曾经在etcd的源码里看到Raft的实现,说实话,代码写得非常优雅。它把状态机、日志、快照都封装得清清楚楚。如果你要自己实现一个分布式系统,我强烈建议直接基于Raft,别自己造轮子。
注意:Raft假设网络是异步的,但节点故障是独立的。如果你的网络经常出现分区,Raft可能会频繁触发Leader选举,导致系统不可用。
1.4 分布式锁:别让资源打架
分布式锁解决的是“多个节点抢同一个资源”的问题。常见的实现方式有三种:
- 基于数据库:用唯一索引或行锁实现。简单但性能差。
- 基于Redis:用SETNX + 过期时间。性能好,但要注意锁超时和RedLock。
- 基于ZooKeeper/etcd:用临时顺序节点。可靠性高,但性能略低。
我个人的习惯是:如果对性能要求极高,用Redis;如果对可靠性要求极高,用etcd。曾经有一次,我用Redis做分布式锁,结果业务处理时间超过了锁的过期时间,锁自动释放了,其他节点抢到了锁,导致数据重复处理。后来我加了一个“锁续期”的机制,才彻底解决这个问题。
// 一个简单的Redis分布式锁示例(伪代码)
bool tryLock(string key, string value, int expireMs) {
// SET NX 保证原子性
return redis.set(key, value, "NX", "PX", expireMs);
}
void unlock(string key, string value) {
// Lua脚本保证原子性:只有持有锁的才能释放
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redis.eval(script, 1, key, value);
}
避坑指南:分布式锁一定要设置过期时间,防止死锁。但过期时间不能太短,否则业务没处理完锁就没了。我一般设置为业务预估时间的3倍。
1.5 服务发现:让服务找到彼此
服务发现解决的是“服务A怎么知道服务B的IP和端口”的问题。常见的方案有:
- 客户端发现:服务启动时注册到注册中心,客户端从注册中心拉取列表。比如Eureka、Consul。
- 服务端发现:客户端请求负载均衡器,负载均衡器从注册中心获取后端服务列表。比如Kubernetes的Service。
我个人更推荐客户端发现模式,因为它少了一层代理,延迟更低。但缺点是需要每个客户端都集成服务发现逻辑。
etcd和Consul是目前最流行的两个服务发现工具:
| 特性 | etcd | Consul |
|---|---|---|
| 一致性协议 | Raft | Raft |
| 健康检查 | 需要自己实现 | 内置 |
| KV存储 | 核心功能 | 支持 |
| DNS接口 | 不支持 | 支持 |
| 多数据中心 | 有限支持 | 原生支持 |
我在项目中用过etcd做服务发现,配合gRPC的负载均衡,效果非常好。Consul我也用过,它的健康检查和DNS接口确实方便,但部署起来比etcd重一些。
总结一下:如果你追求轻量、高性能,选etcd;如果你需要丰富的服务治理功能,选Consul。
知识体系总览
下面这张图把本章的核心知识点串了起来,方便你理解它们之间的关系:
这张图把CAP理论、一致性模型、Raft协议、分布式锁和服务发现串在了一起。你会发现,它们其实都围绕着同一个核心问题——如何在不可靠的网络环境下,构建可靠的分布式系统。