diff --git a/notes/pics/2pc-commit.png b/notes/pics/2pc-commit.png
new file mode 100644
index 00000000..d1c4ede8
Binary files /dev/null and b/notes/pics/2pc-commit.png differ
diff --git a/notes/pics/2pc-prepare.png b/notes/pics/2pc-prepare.png
new file mode 100644
index 00000000..7b31e587
Binary files /dev/null and b/notes/pics/2pc-prepare.png differ
diff --git a/notes/pics/cap-partition.png b/notes/pics/cap-partition.png
new file mode 100644
index 00000000..590f9777
Binary files /dev/null and b/notes/pics/cap-partition.png differ
diff --git a/notes/pics/local-message-table.png b/notes/pics/local-message-table.png
new file mode 100644
index 00000000..a4e67c36
Binary files /dev/null and b/notes/pics/local-message-table.png differ
diff --git a/notes/pics/zk-namespace.png b/notes/pics/zk-namespace.png
new file mode 100644
index 00000000..252413e9
Binary files /dev/null and b/notes/pics/zk-namespace.png differ
diff --git a/notes/分布式.md b/notes/分布式.md
index 9446dd4f..f03ec0a9 100644
--- a/notes/分布式.md
+++ b/notes/分布式.md
@@ -36,7 +36,8 @@
* [八、分布式 ID](#八分布式-id)
* [雪花算法](#雪花算法)
* [九、幂等](#九幂等)
- * [参考](#参考)
+ * [附录:实测](#附录实测)
+ * [参考资料](#参考资料)
@@ -91,11 +92,11 @@ Redis 8.4 新增了 `DELEX lock_key IFEQ unique_value` 指令,可以直接原
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
-- 依次尝试从 N 个互相独立的 Redis 实例获取锁,每次请求的超时时间远小于锁的过期时间;
+- 用同一个键和随机值,并行地向 N 个互相独立的 Redis 实例加锁,每次请求的超时时间远小于锁的过期时间;
- 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功;
- 如果获取锁失败,就到每个实例上释放锁。
-RedLock 依赖各个节点的时钟基本同步,并且无法应对进程暂停(例如 GC)导致锁过期后客户端仍然认为自己持有锁的情况,因此存在争议。更可靠的做法是让锁服务每次加锁时返回一个单调递增的栅栏令牌(Fencing Token),客户端访问资源时带上令牌,资源拒绝令牌比已处理过的更小的请求。
+RedLock 不要求各节点的时钟同步,但假设各节点的时钟走速大致相同,时钟跳变(例如被 NTP 大幅校正)会让锁提前过期;它也无法应对进程暂停(例如 GC)导致锁过期后客户端仍然认为自己持有锁的情况,因此存在争议。更可靠的做法是让锁服务每次加锁时返回一个单调递增的栅栏令牌(Fencing Token),客户端访问资源时带上令牌,资源拒绝令牌比已处理过的更小的请求。
@@ -105,7 +106,7 @@ RedLock 依赖各个节点的时钟基本同步,并且无法应对进程暂停
Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节点为 /app1。
-
+
#### 2. 节点类型
@@ -150,7 +151,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
分布式锁和分布式事务区别:
-- 锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确。
+- 锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确;
- 而事务问题的关键则在于事务涉及的一系列操作需要满足 ACID 特性,例如要满足原子性操作则需要这些操作要么都执行,要么都不执行。
### 2PC
@@ -163,7 +164,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。
-
+
##### 1.2 提交阶段
@@ -173,7 +174,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
协调者在发送通知之前,必须先把决定写入日志,这样崩溃恢复后才知道该继续提交还是回滚。发送失败或者超时时,协调者会不断重试。
-
+
#### 2. 存在的问题
@@ -230,7 +231,7 @@ Saga 没有预留资源的 Try 阶段,中间状态对其它事务可见,因
2. 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除(或者标记为已发送),否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
-
+
由于重新转发会导致消息被重复投递,因此另一方执行的操作需要实现幂等。并且第一方的事务已经提交,无法回滚,所以如果另一方执行失败,只能不断重试或者人工介入处理。
@@ -242,7 +243,7 @@ RocketMQ 的事务消息把本地消息表的工作交给了消息队列:生
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance)。常见的“三选二”说法有误导性,因为网络分区是无法避免的故障,不是可以选择的选项。
-
+
### 一致性
@@ -269,7 +270,7 @@ CAP 中的一致性指的是强一致性(线性一致性):对系统的一
例如两个节点之间发生分区后,一个节点完成了写入,另一个节点收到读请求时,要么返回旧数据(放弃一致性),要么拒绝响应(放弃可用性),不存在第三种选择。
- 为了保证一致性(CP),无法和其它节点同步的节点需要拒绝请求,也就失去了部分可用性,例如 Zookeeper、etcd;
-- 为了保证可用性(AP),所有节点都继续提供服务,但是读取到的数据可能不一致,例如 Eureka、Cassandra。
+- 为了保证可用性(AP),所有节点都继续提供服务,但是读取到的数据可能不一致,例如 Eureka、按默认一致性级别使用的 Cassandra。
PACELC 是对 CAP 的扩展:发生分区(P)时在可用性(A)和一致性(C)之间权衡,没有分区(E)时则在延迟(L)和一致性(C)之间权衡,因为保证强一致性需要等待节点之间同步数据,会增加响应延迟。
@@ -403,7 +404,7 @@ Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了
如果 Follower 的日志和 Leader 不一致,Leader 会找到两者最后一致的位置,然后用自己的日志覆盖 Follower 在该位置之后的日志。
-发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此不会出现脑裂。
+发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此写入不会出现脑裂。读请求则要另外处理:旧 Leader 如果直接用本地数据回答读请求,可能返回旧数据,所以 Raft 要求 Leader 在处理只读请求前,先与大多数节点交换一次心跳,确认自己仍是 Leader。
## 七、RPC
@@ -518,7 +519,7 @@ HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用
| Redis INCR | 用 INCR 得到递增的整数 | 依赖 Redis 的可用性和持久化,宕机后可能重复 |
| 雪花算法 | 本地用时间戳、机器 ID 和序号拼成 64 位整数 | 依赖时钟,需要分配机器 ID |
-在本机的 MySQL 8.0 上分别以自增整数和 UUID() 为主键插入 100 万行:自增主键用时 3.4 秒、数据 133 MB;UUID 主键用时 5.1 秒、数据 164 MB。这是因为 UUID 的值是无序的,每次都插入到 B+ 树中间的随机位置,频繁的页分裂让页的填充率变低,二级索引中保存的主键也更长。
+例如分别以自增整数和 UUID() 为主键插入 100 万行,自增主键用时 3.4 秒、数据 133 MB,UUID 主键用时 5.1 秒、数据 164 MB(MySQL 8.0 实测,见[附录:实测](#附录实测))。这是因为 UUID 的值是无序的,每次都插入到 B+ 树中间的随机位置,频繁的页分裂让页的填充率变低,二级索引中保存的主键也更长。
### 雪花算法
@@ -545,7 +546,7 @@ public synchronized long nextId() {
}
```
-在本机用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有 241 万个不重复。
+用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有约 240 万个不重复(JDK 21 实测,见[附录:实测](#附录实测))。
实际使用时还要解决两个问题:
@@ -565,7 +566,35 @@ public synchronized long nextId() {
需要注意的是,“先查询是否处理过、再处理”分成两步时,两个并发的重复请求可能同时查到“未处理”。判断和处理必须是原子的,要么依靠数据库的唯一索引或条件更新,要么把判断和写入放在同一个 Lua 脚本中。
-## 参考
+## 附录:实测
+
+环境为 Apple M1 Pro、macOS,MySQL 8.0.44,JDK 21。
+
+自增主键与 UUID 主键,每张表插入 100 万行,v 列为 100 个字符:
+
+```sql
+create table t_auto (id bigint primary key auto_increment, v char(100)) engine=innodb;
+create table t_uuid (id char(36) primary key, v char(100)) engine=innodb;
+insert into t_auto (v) with recursive n(i) as (select 1 union all select i + 1 from n where i < 1000000) select repeat('x', 100) from n;
+insert into t_uuid with recursive n(i) as (select 1 union all select i + 1 from n where i < 1000000) select uuid(), repeat('x', 100) from n;
+```
+
+```text
+auto_sec 3.411053
+uuid_sec 5.064563
+TABLE_NAME data_mb free_mb
+t_auto 133 7
+t_uuid 164 7
+```
+
+雪花算法:正文中的 nextId() 补齐字段后,8 个线程各生成 50 万个 ID,检查是否重复、每个线程内是否递增;第二行是去掉“序号用完就等到下一毫秒”之后的结果:
+
+```text
+generated 4000000 unique=4000000 increasingPerThread=true ms=1166
+generated 4000000 unique=2404324 increasingPerThread=false ms=712
+```
+
+## 参考资料
- [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/)
- [Protocol Buffers: Encoding](https://protobuf.dev/programming-guides/encoding/)
@@ -573,10 +602,12 @@ public synchronized long nextId() {
- [gRPC on HTTP/2](https://grpc.io/blog/grpc-on-http2/)
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
-- [Distributed locks with Redis](https://redis.io/topics/distlock)
+- [Distributed Locks with Redis](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/)
- [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://seata.apache.org/zh-cn/blog/seata-tcc/)
+- [Seata TCC 模式的空回滚、幂等与悬挂(tcc-fence)](https://seata.apache.org/zh-cn/blog/seata-tcc-fence/)
+- [Seata Saga 模式](https://seata.apache.org/zh-cn/docs/user/mode/saga/)
- [事务消息 | 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)