mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-29 15:13:44 +00:00
Update 分布式.md
重新绘制配图,补充分布式 ID 和幂等。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
+82
-33
@@ -33,6 +33,9 @@
|
||||
* [服务发现](#服务发现)
|
||||
* [RPC 与 HTTP](#rpc-与-http)
|
||||
* [超时与重试](#超时与重试)
|
||||
* [八、分布式 ID](#八分布式-id)
|
||||
* [雪花算法](#雪花算法)
|
||||
* [九、幂等](#九幂等)
|
||||
* [参考](#参考)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -94,7 +97,7 @@ Redis 8.4 新增了 `DELEX lock_key IFEQ unique_value` 指令,可以直接原
|
||||
|
||||
RedLock 依赖各个节点的时钟基本同步,并且无法应对进程暂停(例如 GC)导致锁过期后客户端仍然认为自己持有锁的情况,因此存在争议。更可靠的做法是让锁服务每次加锁时返回一个单调递增的栅栏令牌(Fencing Token),客户端访问资源时带上令牌,资源拒绝令牌比已处理过的更小的请求。
|
||||
|
||||
<div align="center"> <img src="pics/fencing-token.png" width="560px"/> </div><br>
|
||||
<div align="center"> <img src="pics/fencing-token.png" width="600px"/> </div><br>
|
||||
|
||||
### Zookeeper 的有序节点
|
||||
|
||||
@@ -215,7 +218,7 @@ TCC(Try-Confirm-Cancel)是在业务层面实现的两阶段提交,每个
|
||||
|
||||
Saga 将一个分布式事务拆分为多个本地事务,每个本地事务直接提交,并且都有一个对应的补偿操作。依次执行各个本地事务,如果某一个失败,就逆序执行前面已经提交的本地事务的补偿操作。
|
||||
|
||||
<div align="center"> <img src="pics/saga.png" width="540px"/> </div><br>
|
||||
<div align="center"> <img src="pics/saga.png" width="600px"/> </div><br>
|
||||
|
||||
Saga 没有预留资源的 Try 阶段,中间状态对其它事务可见,因此没有隔离性。它适合流程长、无法长时间锁定资源的业务,正向操作和补偿操作同样需要实现幂等。
|
||||
|
||||
@@ -233,7 +236,7 @@ Saga 没有预留资源的 Try 阶段,中间状态对其它事务可见,因
|
||||
|
||||
RocketMQ 的事务消息把本地消息表的工作交给了消息队列:生产者先发送一条对消费者不可见的半消息,再执行本地事务,然后根据执行结果通知 Broker 提交或者回滚这条消息。如果 Broker 长时间没有收到通知,会主动回查生产者的本地事务状态。
|
||||
|
||||
<div align="center"> <img src="pics/rocketmq-transaction.png" width="540px"/> </div><br>
|
||||
<div align="center"> <img src="pics/rocketmq-transaction.png" width="600px"/> </div><br>
|
||||
|
||||
## 三、CAP
|
||||
|
||||
@@ -306,7 +309,7 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE
|
||||
|
||||
一个节点可以同时担任多种角色。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b988877c-0f0a-4593-916d-de2081320628.jpg"/> </div><br>
|
||||
<div align="center"> <img src="pics/paxos-roles.png" width="720px"/> </div><br>
|
||||
|
||||
### 执行过程
|
||||
|
||||
@@ -343,7 +346,7 @@ Acceptor 接受一个提议之后,会将该提议发送给所有的 Learner。
|
||||
- A 发送 Prepare(2),X 和 Y 做出承诺并响应。A 得到大多数响应之后发送 Accept(2, 8),X 和 Y 接受该提议,此时值 8 已经被选定;
|
||||
- 之后 B 发送 Prepare(4),它收到的大多数响应中至少有一个来自 X 或 Y,响应中包含已经接受过的提议 [2, 8],因此 B 只能发送 Accept(4, 8),而不能使用自己的提议值,从而保证了选定的值不会改变。
|
||||
|
||||
<div align="center"> <img src="pics/paxos-example.png" width="600px"/> </div><br>
|
||||
<div align="center"> <img src="pics/paxos-example.png" width="640px"/> </div><br>
|
||||
|
||||
### 约束条件
|
||||
|
||||
@@ -371,51 +374,32 @@ Raft 也是分布式共识算法,相比于 Paxos 更容易理解和实现。
|
||||
|
||||
Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,将任期号加 1 并给自己投一票,进入竞选阶段。
|
||||
|
||||
- 下图展示一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521118015898.gif"/> </div><br>
|
||||
|
||||
- 此时 Node A 发送投票请求给其它所有节点。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521118445538.gif"/> </div><br>
|
||||
- 系统刚启动时只有 Follower 没有 Leader。Node A 的随机竞选超时最短,到期时没收到 Leader 发来的心跳包,因此进入竞选阶段,发送投票请求给其它所有节点。
|
||||
|
||||
- 每个节点在一个任期内最多只能投一票,先到先得,并且只会投给日志至少和自己一样新的 Candidate,这保证了新的 Leader 一定包含所有已提交的日志。如果 Candidate 获得超过一半节点的选票,就会变成 Leader。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521118483039.gif"/> </div><br>
|
||||
|
||||
- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521118640738.gif"/> </div><br>
|
||||
<div align="center"> <img src="pics/raft-election.png" width="760px"/> </div><br>
|
||||
|
||||
### 多个 Candidate 竞选
|
||||
|
||||
- 如果有多个 Follower 同时成为 Candidate,选票可能会被瓜分,导致没有 Candidate 获得超过一半的选票,那么等待竞选超时之后进入下一个任期重新开始投票。例如下图中 Node B 和 Node D 都获得两票,需要重新开始投票。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521119203347.gif"/> </div><br>
|
||||
|
||||
- 由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111521119368714.gif"/> </div><br>
|
||||
<div align="center"> <img src="pics/raft-split.png" width="760px"/> </div><br>
|
||||
|
||||
- 如果 Candidate 在竞选过程中收到了任期号不小于自己的 Leader 发来的心跳包,就会承认该 Leader 并变回 Follower。
|
||||
|
||||
### 日志复制
|
||||
|
||||
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/71550414107576.gif"/> </div><br>
|
||||
|
||||
- Leader 会把修改复制到所有 Follower。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/91550414131331.gif"/> </div><br>
|
||||
|
||||
- Leader 会等待大多数的节点都写入了该日志,然后才将修改提交,并向客户端返回结果。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/101550414151983.gif"/> </div><br>
|
||||
|
||||
- 此时 Leader 会在之后的心跳包中通知所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/111550414182638.gif"/> </div><br>
|
||||
<div align="center"> <img src="pics/raft-log.png" width="760px"/> </div><br>
|
||||
|
||||
如果 Follower 的日志和 Leader 不一致,Leader 会找到两者最后一致的位置,然后用自己的日志覆盖 Follower 在该位置之后的日志。
|
||||
|
||||
@@ -516,6 +500,71 @@ HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用
|
||||
- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重,否则重试会扣减两次;
|
||||
- 调用链较长时,下游的超时时间应该比上游短。gRPC 会把截止时间(deadline)随请求传给服务方,超时后调用方收到 DEADLINE_EXCEEDED,服务方也可以检查调用是否已经超时,从而停止处理。
|
||||
|
||||
## 八、分布式 ID
|
||||
|
||||
单库时用数据库的自增主键就能生成 ID,分库分表之后,各个库的自增序列会重复,需要一个全局唯一的 ID。一般有以下要求:
|
||||
|
||||
- 全局唯一;
|
||||
- 大体按时间递增,作为 InnoDB 的主键时能顺序插入;
|
||||
- 生成快、可用性高,不能成为瓶颈;
|
||||
- 有时还要求不能从 ID 推测出业务量,例如订单号不能是连续的数字。
|
||||
|
||||
常用的方案如下:
|
||||
|
||||
| 方案 | 做法 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| UUID | 本地生成 128 位随机数 | 无序,作主键时随机插入、页分裂多;36 个字符太长 |
|
||||
| 数据库号段 | 用一张表记录当前最大 ID,每次取一段(例如 1000 个)放在内存中慢慢用 | 依赖数据库,服务重启会浪费没用完的号段 |
|
||||
| Redis INCR | 用 INCR 得到递增的整数 | 依赖 Redis 的可用性和持久化,宕机后可能重复 |
|
||||
| 雪花算法 | 本地用时间戳、机器 ID 和序号拼成 64 位整数 | 依赖时钟,需要分配机器 ID |
|
||||
|
||||
在本机的 MySQL 8.0 上分别以自增整数和 UUID() 为主键插入 100 万行:自增主键用时 3.4 秒、数据 133 MB;UUID 主键用时 5.1 秒、数据 164 MB。这是因为 UUID 的值是无序的,每次都插入到 B+ 树中间的随机位置,频繁的页分裂让页的填充率变低,二级索引中保存的主键也更长。
|
||||
|
||||
### 雪花算法
|
||||
|
||||
雪花算法(Snowflake)把 64 位整数分成四部分:
|
||||
|
||||
<div align="center"> <img src="pics/snowflake.png" width="760px"/> </div><br>
|
||||
|
||||
```java
|
||||
public synchronized long nextId() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now < lastTime) { // 时钟回拨
|
||||
throw new IllegalStateException("clock moved backwards");
|
||||
}
|
||||
if (now == lastTime) {
|
||||
seq = (seq + 1) & 4095; // 同一毫秒内序号加 1
|
||||
if (seq == 0) { // 4096 个用完,等到下一毫秒
|
||||
while ((now = System.currentTimeMillis()) <= lastTime) { }
|
||||
}
|
||||
} else {
|
||||
seq = 0;
|
||||
}
|
||||
lastTime = now;
|
||||
return ((now - EPOCH) << 22) | (workerId << 12) | seq;
|
||||
}
|
||||
```
|
||||
|
||||
在本机用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有 241 万个不重复。
|
||||
|
||||
实际使用时还要解决两个问题:
|
||||
|
||||
- 机器 ID:每个实例的机器 ID 不能相同,可以在启动时从 ZooKeeper、数据库或配置中心领取;
|
||||
- 时钟回拨:服务器校时可能让时钟倒退,回拨较小时可以等待时钟追上,较大时报错,或者像美团的 Leaf 那样在启动时检查本机时间与其它节点是否一致。
|
||||
|
||||
## 九、幂等
|
||||
|
||||
幂等指的是同一个操作执行一次和执行多次的效果相同。分布式系统中重复的请求很常见:调用超时后重试、用户重复点击、消息队列重复投递,都会让同一个操作执行多次。查询和删除天然幂等,新增和扣减不是,例如支付回调被调用两次,订单就可能被处理两次。
|
||||
|
||||
实现方法:
|
||||
|
||||
- 唯一索引:为每个请求生成唯一的请求 ID,在去重表的主键或唯一索引上插入,重复的请求插入失败(ERROR 1062 Duplicate entry)就直接返回之前的结果,去重表和业务数据在同一个本地事务中写入;
|
||||
- 状态机:更新时带上前置状态,例如 UPDATE orders SET status = 'PAID' WHERE id = 1024 AND status = 'UNPAID',第一次影响 1 行,重复执行影响 0 行;
|
||||
- 版本号:更新时带上读到的版本号,版本号不匹配就不更新,即乐观锁;
|
||||
- Token:提交前先从服务端获取一个 Token 存入 Redis,提交时用 DEL 删除,返回 1 才处理,重复提交时返回 0。
|
||||
|
||||
需要注意的是,“先查询是否处理过、再处理”分成两步时,两个并发的重复请求可能同时查到“未处理”。判断和处理必须是原子的,要么依靠数据库的唯一索引或条件更新,要么把判断和写入放在同一个 Lua 脚本中。
|
||||
|
||||
## 参考
|
||||
|
||||
- [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/)
|
||||
@@ -527,16 +576,16 @@ HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用
|
||||
- [Distributed locks with Redis](https://redis.io/topics/distlock)
|
||||
- [How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html)
|
||||
- [Locks and synchronizers - Redisson](https://redisson.pro/docs/data-and-services/locks-and-synchronizers/)
|
||||
- [深度剖析 Seata TCC 模式](https://www.seata.io/zh-cn/blog/seata-tcc/)
|
||||
- [深度剖析 Seata TCC 模式](https://seata.apache.org/zh-cn/blog/seata-tcc/)
|
||||
- [事务消息 | RocketMQ](https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage/)
|
||||
- [An Illustrated Proof of the CAP Theorem](https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/)
|
||||
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
|
||||
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
|
||||
- [基于 Zookeeper 的分布式锁](https://web.archive.org/web/20221129202405/http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
|
||||
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
|
||||
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
|
||||
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
|
||||
- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/)
|
||||
- [What is CAP theorem in distributed database system?](https://web.archive.org/web/20170428094507/http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/)
|
||||
- [Paxos Made Simple](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf)
|
||||
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
|
||||
- [NEAT ALGORITHMS - PAXOS](https://web.archive.org/web/20260307090655/http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
|
||||
- [In Search of an Understandable Consensus Algorithm](https://raft.github.io/raft.pdf)
|
||||
|
||||
- [Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html)
|
||||
|
||||
Reference in New Issue
Block a user