Update 分布式.md

重新绘制配图,补充分布式 ID 和幂等。
This commit is contained in:
CyC2018
2026-10-26 22:04:03 +08:00
parent c942f9509a
commit 07db1f5e9e
11 changed files with 421 additions and 33 deletions
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
View File
@@ -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)
+58
View File
@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="250" viewBox="0 0 720 250">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="360" y="28" class="title">Paxos 的三类角色</text>
<rect x="40" y="70" width="130" height="36" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="105" y="92.5" class="c sub b">Proposer A</text>
<rect x="40" y="150" width="130" height="36" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="105" y="172.5" class="c sub b">Proposer B</text>
<rect x="300" y="50" width="130" height="36" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="365" y="72.5" class="c sub b">Acceptor X</text>
<rect x="300" y="110" width="130" height="36" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="365" y="132.5" class="c sub b">Acceptor Y</text>
<rect x="300" y="170" width="130" height="36" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="365" y="192.5" class="c sub b">Acceptor Z</text>
<rect x="560" y="110" width="120" height="36" rx="6" fill="#EAE9E7" stroke="#BFBEBA"/>
<text x="620" y="132.5" class="c sub b">Learner</text>
<line x1="172" y1="88" x2="298" y2="68" class="arrow "/>
<line x1="172" y1="88" x2="298" y2="128" class="arrow "/>
<line x1="172" y1="88" x2="298" y2="188" class="arrow "/>
<line x1="172" y1="168" x2="298" y2="68" class="arrow "/>
<line x1="172" y1="168" x2="298" y2="128" class="arrow "/>
<line x1="172" y1="168" x2="298" y2="188" class="arrow "/>
<line x1="432" y1="68" x2="558" y2="128" class="arrow dash"/>
<line x1="432" y1="128" x2="558" y2="128" class="arrow dash"/>
<line x1="432" y1="188" x2="558" y2="128" class="arrow dash"/>
<text x="235" y="46" class="c sub">Prepare、Accept</text>
<text x="495" y="66" class="c sub">接受的提议</text>
<text x="30" y="236" class="sub">Proposer 向所有 Acceptor 发请求,超过一半 Acceptor 接受同一个提议,这个值就被选定;Learner 只负责得知结果。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+74
View File
@@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="350" viewBox="0 0 760 350">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">Raft 选举:A 的竞选计时器最先到期</text>
<text x="40" y="84" class="sub b" style="fill:#444">节点 A</text>
<text x="40" y="100" class="sub">超时 180 ms</text>
<rect x="150" y="72" width="247.5" height="16" rx="3" fill="#EAE9E7" stroke="#BFBEBA" stroke-width="1"/>
<text x="273.75" y="84" class="c sub">Follower,等待心跳</text>
<rect x="397.5" y="72" width="22" height="16" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<rect x="419.5" y="72" width="280.5" height="16" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="562.5" y="84" class="c sub b" style="fill:#444">Leader,任期 1</text>
<text x="40" y="154" class="sub b" style="fill:#444">节点 B</text>
<text x="40" y="170" class="sub">超时 260 ms</text>
<rect x="150" y="142" width="261.25" height="16" rx="3" fill="#EAE9E7" stroke="#BFBEBA" stroke-width="1"/>
<text x="273.75" y="154" class="c sub">Follower</text>
<rect x="419.5" y="142" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="488.25" y="142" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="557" y="142" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="625.75" y="142" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="694.5" y="142" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<line x1="507.5" y1="138" x2="507.5" y2="162" stroke="#C0605A" stroke-dasharray="2 2"/>
<text x="507.5" y="176" class="c sub">原定 260 ms 超时</text>
<text x="40" y="224" class="sub b" style="fill:#444">节点 C</text>
<text x="40" y="240" class="sub">超时 230 ms</text>
<rect x="150" y="212" width="261.25" height="16" rx="3" fill="#EAE9E7" stroke="#BFBEBA" stroke-width="1"/>
<text x="273.75" y="224" class="c sub">Follower</text>
<rect x="419.5" y="212" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="488.25" y="212" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="557" y="212" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="625.75" y="212" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<rect x="694.5" y="212" width="4" height="16" rx="1" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<line x1="466.25" y1="208" x2="466.25" y2="232" stroke="#C0605A" stroke-dasharray="2 2"/>
<text x="466.25" y="246" class="c sub">原定 230 ms 超时</text>
<text x="397.5" y="64" class="sub" style="fill:#766AA8">180 ms 超时:成为候选人,任期加 1,投自己一票,向 B、C 请求投票</text>
<line x1="408.5" y1="90" x2="408.5" y2="210" stroke="#B8B0DE" stroke-dasharray="3 3"/>
<text x="402.5" y="119" class="sub e">B、C 投票给 A,3 票中得到 3 票</text>
<text x="562.5" y="268" class="c sub">浅青色短条是 Leader 当选后立即发出、之后每 50 ms 发一次的心跳,Follower 收到后重新计时</text>
<line x1="150" y1="292" x2="700" y2="292" class="line"/>
<text x="150" y="310" class="c sub mono">0 ms</text>
<text x="287.5" y="310" class="c sub mono">100 ms</text>
<text x="425" y="310" class="c sub mono">200 ms</text>
<text x="562.5" y="310" class="c sub mono">300 ms</text>
<text x="700" y="310" class="c sub mono">400 ms</text>
<text x="30" y="338" class="sub">每个节点的竞选超时在 150~300 ms 中随机选取,通常只有一个节点先超时,它在别人超时前就能拿到多数票。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

+88
View File
@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="330" viewBox="0 0 760 330">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">Raft 日志复制:SET x = 5</text>
<text x="162" y="56" class="c sub b" style="fill:#444">① 写入 Leader 的日志</text>
<text x="80" y="91" class="sub b e" style="fill:#444">Leader</text>
<rect x="90" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="126" y="90" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="162" y="72" width="72" height="28" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="198" y="90" class="c sub mono" style="fill:#444">1 x=5</text>
<text x="80" y="133" class="sub b e" style="fill:#444">B</text>
<rect x="90" y="114" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="126" y="132" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="162" y="114" width="72" height="28" rx="3" fill="#FFFFFF" stroke="#BBBBBB" stroke-width="1"/>
<text x="80" y="175" class="sub b e" style="fill:#444">C</text>
<rect x="90" y="156" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="126" y="174" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="162" y="156" width="72" height="28" rx="3" fill="#FFFFFF" stroke="#BBBBBB" stroke-width="1"/>
<text x="332" y="56" class="c sub b" style="fill:#444">② 复制给 Follower</text>
<rect x="260" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="296" y="90" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="332" y="72" width="72" height="28" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="368" y="90" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="260" y="114" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="296" y="132" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="332" y="114" width="72" height="28" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="368" y="132" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="260" y="156" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="296" y="174" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="332" y="156" width="72" height="28" rx="3" fill="#FFFFFF" stroke="#BBBBBB" stroke-width="1"/>
<text x="502" y="56" class="c sub b" style="fill:#444">③ 多数写入后提交</text>
<rect x="430" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="466" y="90" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="502" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="538" y="90" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="430" y="114" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="466" y="132" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="502" y="114" width="72" height="28" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="538" y="132" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="430" y="156" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="466" y="174" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="502" y="156" width="72" height="28" rx="3" fill="#FFFFFF" stroke="#BBBBBB" stroke-width="1"/>
<text x="672" y="56" class="c sub b" style="fill:#444">④ 心跳通知 Follower 提交</text>
<rect x="600" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="636" y="90" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="672" y="72" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="708" y="90" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="600" y="114" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="636" y="132" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="672" y="114" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="708" y="132" class="c sub mono" style="fill:#444">1 x=5</text>
<rect x="600" y="156" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="636" y="174" class="c sub mono" style="fill:#444">1 y=1</text>
<rect x="672" y="156" width="72" height="28" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="708" y="174" class="c sub mono" style="fill:#444">1 x=5</text>
<text x="30" y="236" class="sub">格子中是任期号和命令。浅青色已提交,淡紫色已写入日志但未提交。</text>
<text x="30" y="254" class="sub">第 ③ 步时 C 还没有写入,但 Leader 和 B 已经构成 3 个节点中的多数,Leader 就可以提交、执行并回复客户端;</text>
<text x="30" y="272" class="sub">已提交的日志一定在多数节点上,新 Leader 也一定包含它,所以不会丢失。C 落后或宕机恢复后,Leader 再把缺少的日志补给它。</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

+63
View File
@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="300" viewBox="0 0 760 300">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">Raft 选举:选票被瓜分时进入下一个任期</text>
<text x="30" y="92" class="b">任期 4</text>
<rect x="110" y="70" width="130" height="40" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="175" y="88" class="c sub b" style="fill:#444">节点 A</text>
<text x="175" y="103" class="c sub">投 B</text>
<rect x="260" y="70" width="130" height="40" rx="6" fill="#E6E3FA" stroke="#B8B0DE"/>
<text x="325" y="88" class="c sub b" style="fill:#444">节点 B</text>
<text x="325" y="103" class="c sub">候选人,2 票</text>
<rect x="410" y="70" width="130" height="40" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="475" y="88" class="c sub b" style="fill:#444">节点 C</text>
<text x="475" y="103" class="c sub">投 D</text>
<rect x="560" y="70" width="130" height="40" rx="6" fill="#E6E3FA" stroke="#B8B0DE"/>
<text x="625" y="88" class="c sub b" style="fill:#444">节点 D</text>
<text x="625" y="103" class="c sub">候选人,2 票</text>
<text x="110" y="132" class="sub" style="">B、D 同时超时,各得 2 票,都不足 3 票(4 个节点的多数),没有 Leader</text>
<text x="30" y="202" class="b">任期 5</text>
<rect x="110" y="180" width="130" height="40" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="175" y="198" class="c sub b" style="fill:#444">节点 A</text>
<text x="175" y="213" class="c sub">投 D</text>
<rect x="260" y="180" width="130" height="40" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="325" y="198" class="c sub b" style="fill:#444">节点 B</text>
<text x="325" y="213" class="c sub">投 D</text>
<rect x="410" y="180" width="130" height="40" rx="6" fill="#C6E4FB" stroke="#7FB2D6"/>
<text x="475" y="198" class="c sub b" style="fill:#444">节点 C</text>
<text x="475" y="213" class="c sub">投 D</text>
<rect x="560" y="180" width="130" height="40" rx="6" fill="#C6ECE9" stroke="#7DBCB7"/>
<text x="625" y="198" class="c sub b" style="fill:#444">节点 D</text>
<text x="625" y="213" class="c sub">候选人,4 票</text>
<text x="110" y="242" class="sub" style="fill:#444">重新随机超时,D 先超时,得到全部选票,成为 Leader</text>
<text x="30" y="284" class="sub">淡紫色是候选人,浅青色是当选的 Leader。随机超时让两个节点再次同时超时的概率很低。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+56
View File
@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="300" viewBox="0 0 760 300">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #3F3F3F; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #333; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.sub { font-size: 11px; fill: #6E6E6E; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #555; stroke-width: 1.1; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #555; stroke-width: 1.1; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#555"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#C0605A"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1.5 L9.5,5 L1,8.5 z" fill="#6A9F68"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="380" y="28" class="title">雪花算法:64 位 ID 的组成</text>
<rect x="30" y="70" width="34" height="36" rx="3" fill="#EAE9E7" stroke="#BFBEBA" stroke-width="1"/>
<text x="47" y="92" class="c sub mono" style="fill:#444">0</text>
<text x="47" y="62" class="c sub b" style="fill:#444">符号位</text>
<text x="47" y="124" class="c sub">1 位,固定为 0</text>
<rect x="66" y="70" width="434.6" height="36" rx="3" fill="#C6E4FB" stroke="#7FB2D6" stroke-width="1"/>
<text x="283.3" y="92" class="c sub mono" style="fill:#444">00001010010110100100001001111001110111101</text>
<text x="283.3" y="62" class="c sub b" style="fill:#444">时间戳</text>
<text x="283.3" y="124" class="c sub">41 位:距 2024-01-01 的毫秒数</text>
<rect x="502.6" y="70" width="106" height="36" rx="3" fill="#C6ECE9" stroke="#7DBCB7" stroke-width="1"/>
<text x="555.6" y="92" class="c sub mono" style="fill:#444">0000000101</text>
<text x="555.6" y="62" class="c sub b" style="fill:#444">机器 ID</text>
<text x="555.6" y="124" class="c sub">10 位:0~1023</text>
<rect x="610.6" y="70" width="127.2" height="36" rx="3" fill="#E6E3FA" stroke="#B8B0DE" stroke-width="1"/>
<text x="674.2" y="92" class="c sub mono" style="fill:#444">000000000000</text>
<text x="674.2" y="62" class="c sub b" style="fill:#444">序号</text>
<text x="674.2" y="124" class="c sub">12 位:同一毫秒内 0~4095</text>
<text x="30" y="164" class="sub b" style="fill:#444">本机生成的一个 ID:372990889737342976</text>
<text x="30" y="184" class="sub mono" style="fill:#444">时间戳 88927957949 ms → 2026-10-26 06:12:37.949(UTC),机器 ID 5,序号 0</text>
<text x="30" y="214" class="sub">41 位毫秒数可以用 69.7 年;每台机器每毫秒最多 4096 个,即每秒约 409 万个。</text>
<text x="30" y="232" class="sub">高位是时间,多台机器生成的 ID 整体上按时间递增,作为 InnoDB 主键时基本是顺序插入。</text>
<text x="30" y="256" class="sub note">需要注意的是,ID 依赖本机时钟:时钟回拨后可能生成重复的 ID,生成器发现当前时间小于上次的时间时,</text>
<text x="30" y="274" class="sub note">回拨很小就等待追上,较大就报错,由运维排查时钟同步。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB