diff --git a/notes/pics/fencing-token.png b/notes/pics/fencing-token.png index cf89866b..8f5e883d 100644 Binary files a/notes/pics/fencing-token.png and b/notes/pics/fencing-token.png differ diff --git a/notes/分布式.md b/notes/分布式.md index f03ec0a9..94196ef6 100644 --- a/notes/分布式.md +++ b/notes/分布式.md @@ -59,7 +59,7 @@ 存在以下几个问题: - 锁没有失效时间,如果持有锁的进程崩溃导致解锁失败,其它进程就无法再获得该锁; -- 只能是非阻塞锁,插入失败直接就报错了,无法重试; +- 只能是非阻塞锁,插入失败直接就报错了,需要等待锁时只能由调用方循环重试; - 不可重入,已经获得锁的进程也必须重新获取锁。 ### Redis 的 SET NX 指令 @@ -96,10 +96,24 @@ Redis 8.4 新增了 `DELEX lock_key IFEQ unique_value` 指令,可以直接原 - 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功; - 如果获取锁失败,就到每个实例上释放锁。 -RedLock 不要求各节点的时钟同步,但假设各节点的时钟走速大致相同,时钟跳变(例如被 NTP 大幅校正)会让锁提前过期;它也无法应对进程暂停(例如 GC)导致锁过期后客户端仍然认为自己持有锁的情况,因此存在争议。更可靠的做法是让锁服务每次加锁时返回一个单调递增的栅栏令牌(Fencing Token),客户端访问资源时带上令牌,资源拒绝令牌比已处理过的更小的请求。 +RedLock 不要求各节点的时钟同步,但假设各节点的时钟走速大致相同,时钟跳变(例如被 NTP 大幅校正)会让锁提前过期,这是它存在争议的原因之一。 + +更根本的问题在于,所有带过期时间的锁都可能同时被两个客户端持有:持有锁的客户端发生长时间的进程暂停(例如 GC),或者它发出的请求在网络中延迟了很久,锁过期后另一个客户端已经拿到了锁,前一个客户端的写入却仍然会到达资源。 + +解决方法是让锁服务每次加锁时返回一个单调递增的栅栏令牌(Fencing Token),客户端访问资源时带上令牌,资源拒绝令牌比已处理过的更小的请求。

+例如订单服务 A 拿到令牌 7 后发生了 12 秒的 Full GC,锁在 10 秒后过期,订单服务 B 拿到令牌 8 并完成写入;A 恢复后带着令牌 7 写入,就会被拒绝。 + +资源是 MySQL 中的一行时,可以把处理过的最大令牌保存在这一行的 fence 列中,用条件更新完成检查。B 执行下面的语句后 fence 变为 8,A 带着令牌 7 执行同样的语句(条件为 fence <= 7)时影响 0 行: + +```sql +UPDATE inventory SET stock = 97, fence = 8 WHERE sku_id = 10086 AND fence <= 8; +``` + +RedLock 使用的随机值不是递增的,算法本身无法提供栅栏令牌;ZooKeeper 则可以使用加锁时创建的有序节点的序号或者事务 ID(zxid),它们都是单调递增的。 + ### Zookeeper 的有序节点 #### 1. Zookeeper 抽象模型 @@ -116,7 +130,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节 #### 3. 监听器 -为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 +为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。监听是一次性的,触发之后需要重新注册(3.6 起也支持触发后不删除的持久监听)。 #### 4. 分布式锁实现 @@ -129,7 +143,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节 如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,这种实现方式不会出现数据库的唯一索引实现方式释放锁失败的问题。 -但是如果客户端因为 GC 等原因长时间没有发送心跳,也会导致会话超时,因此同样可能出现两个客户端都认为自己持有锁的情况。 +但是如果客户端因为 GC 等原因长时间没有发送心跳,也会导致会话超时,因此同样可能出现两个客户端都认为自己持有锁的情况,需要配合上文的栅栏令牌。 #### 6. 羊群效应 @@ -139,7 +153,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节 - 数据库:实现简单,但是性能低; - Redis:性能最高,适合高并发场景,但是在主从切换、进程暂停等极端情况下可能出现两个客户端同时持有锁; -- Zookeeper:写操作需要同步到大多数节点,性能较低,但是可靠性最高,适合一致性要求高、并发量不大的场景。 +- Zookeeper:写操作需要同步到大多数节点,性能较低,但是可靠性更高,还能提供栅栏令牌,适合一致性要求高、并发量不大的场景。 实际使用中一般直接采用 Redisson、Curator 等成熟的客户端实现。 @@ -162,7 +176,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节 ##### 1.1 准备阶段 -协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。 +协调者询问每个参与者能否提交。参与者先执行事务,确认在任何情况下都能提交(数据已经写入磁盘、约束检查已经通过)才回复同意,否则回复中止。询问可以看成一种投票,需要参与者都同意才能提交。

@@ -194,7 +208,7 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 -三阶段提交(3PC)在 2PC 的基础上增加了一个询问阶段(CanCommit),并且在参与者端也引入了超时机制,参与者在等待提交通知超时后会默认提交,从而减少了阻塞。但是它假设网络延迟有上界,在现实系统中仍然可能出现数据不一致。实践中更好的办法是用共识算法代替单点的协调者。 +三阶段提交(3PC)在 2PC 的基础上增加了一个询问阶段(CanCommit),并且在参与者端也引入了超时机制:参与者收到预提交(PreCommit)之后,如果等待最终通知超时,就默认提交,从而减少了阻塞。但是它假设网络延迟和节点的响应时间都有上界,而现实系统中存在无界的网络延迟和进程暂停,超时并不能说明对方已经失效,因此仍然可能出现数据不一致。实践中更常见的做法是用共识算法把协调者的决定复制到多个节点上,避免单点。 ### TCC @@ -247,7 +261,11 @@ RocketMQ 的事务消息把本地消息表的工作交给了消息队列:生 ### 一致性 -CAP 中的一致性指的是强一致性(线性一致性):对系统的一个数据更新成功之后,所有用户都能够读取到最新的值,就像系统中只有一个数据副本一样。 +CAP 中的一致性指的是线性一致性(Linearizability),也称为强一致性:系统看起来就像只有一个数据副本,每个操作都在某个时刻原子地生效。 + +一个数据更新成功之后,所有用户都能读到新值;并且只要有一个读请求读到了新值,之后的读请求就都不能再读到旧值,即使这时写操作还没有返回。 + +例如订单 1024 的状态从“待支付”改为“已支付”,写入还在多个副本之间同步时,客服 A 查询看到了“已支付”并告诉客服 B,B 随后查询却看到“待支付”。两次查询分别落在已同步和未同步的副本上,这在线性一致的系统中是不允许的。 注意它和 ACID 中的一致性不是同一个概念,ACID 中的一致性指的是事务执行前后,数据都满足完整性约束。 @@ -272,6 +290,12 @@ CAP 中的一致性指的是强一致性(线性一致性):对系统的一 - 为了保证一致性(CP),无法和其它节点同步的节点需要拒绝请求,也就失去了部分可用性,例如 Zookeeper、etcd; - 为了保证可用性(AP),所有节点都继续提供服务,但是读取到的数据可能不一致,例如 Eureka、按默认一致性级别使用的 Cassandra。 +需要注意的是,ZooKeeper 只保证写入是线性一致的,读请求由客户端连接的节点直接返回,可能读到旧值,需要读到最新值时要先调用 sync();etcd 从 v3 起默认提供线性一致的读。 + +Nacos 则两种都有:临时实例使用自研的 Distro 协议,优先保证可用性;持久实例使用基于 Raft 的 JRaft,优先保证一致性。 + +CAP 只讨论了线性一致性这一种一致性模型和网络分区这一种故障,没有涉及网络延迟、节点宕机等更常见的问题。很多系统既不提供线性一致性,也不满足 CAP 中严格定义的可用性,因此既不是 CP 也不是 AP。与其给系统贴上 CP 或 AP 的标签,不如直接说明它在分区时的行为,例如“少数派一侧拒绝写入,读请求可能返回旧值”。 + PACELC 是对 CAP 的扩展:发生分区(P)时在可用性(A)和一致性(C)之间权衡,没有分区(E)时则在延迟(L)和一致性(C)之间权衡,因为保证强一致性需要等待节点之间同步数据,会增加响应延迟。 ## 四、BASE @@ -294,7 +318,7 @@ BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。“最终”没有时间上限,因此通常还会提供读己之写(用户能读到自己刚写入的数据)和单调读(不会读到比上次更旧的数据)等保证来减少不一致带来的困扰。 -ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。 +ACID 要求每个事务结束时数据都满足约束,并且事务之间相互隔离,通常运用在传统的数据库系统上。而 BASE 允许数据在一段时间内处于中间状态,只要求最终一致,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。 在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 @@ -404,7 +428,9 @@ Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了 如果 Follower 的日志和 Leader 不一致,Leader 会找到两者最后一致的位置,然后用自己的日志覆盖 Follower 在该位置之后的日志。 -发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此写入不会出现脑裂。读请求则要另外处理:旧 Leader 如果直接用本地数据回答读请求,可能返回旧数据,所以 Raft 要求 Leader 在处理只读请求前,先与大多数节点交换一次心跳,确认自己仍是 Leader。 +发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此写入不会出现脑裂。 + +读请求则要另外处理:旧 Leader 如果直接用本地数据回答读请求,可能返回旧数据,所以 Raft 要求 Leader 在处理只读请求前,先与大多数节点交换一次心跳,确认自己仍是 Leader。另外,新 Leader 刚上任时还不知道哪些日志已经提交,要先提交一条当前任期的空日志,之后才能处理只读请求。 ## 七、RPC @@ -548,6 +574,8 @@ public synchronized long nextId() { 用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有约 240 万个不重复(JDK 21 实测,见[附录:实测](#附录实测))。 +需要注意的是,同一个实例生成的 ID 是递增的,但不同实例的时钟之间存在偏差,两个实例生成的 ID 只能大致反映生成的先后顺序。 + 实际使用时还要解决两个问题: - 机器 ID:每个实例的机器 ID 不能相同,可以在启动时从 ZooKeeper、数据库或配置中心领取; @@ -604,6 +632,9 @@ generated 4000000 unique=2404324 increasingPerThread=false ms=712 - [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia) - [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) +- [ZooKeeper Programmer's Guide](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html) +- [etcd: KV API guarantees](https://etcd.io/docs/v3.5/learning/api_guarantees/) +- [Nacos 的 Distro 一致性协议](https://nacos.io/en/news/article-nacos-distro-mechanism/) - [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/)