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)