Update 分布式.md

为面试中常问的小节加上高频标记;补充分布式事务几种做法的对比,以及用请求 ID 实现幂等的示意图。
This commit is contained in:
CyC2018
2026-10-30 02:57:53 +08:00
parent c0af968f9e
commit 71868643c6
2 changed files with 46 additions and 19 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+46 -19
View File
@@ -1,17 +1,18 @@
# 分布式
<!-- GFM-TOC -->
* [分布式](#分布式)
* [一、分布式锁](#一分布式锁)
* [一、分布式锁](#一分布式锁) 🔥🔥
* [数据库的唯一索引](#数据库的唯一索引)
* [Redis 的 SET NX 指令](#redis-的-set-nx-指令)
* [Redis 的 RedLock 算法](#redis-的-redlock-算法)
* [Zookeeper 的有序节点](#zookeeper-的有序节点)
* [三种实现的对比](#三种实现的对比)
* [二、分布式事务](#二分布式事务)
* [二、分布式事务](#二分布式事务) 🔥
* [2PC](#2pc)
* [TCC](#tcc)
* [Saga](#saga)
* [本地消息表](#本地消息表)
* [几种做法的对比](#几种做法的对比)
* [三、CAP](#三cap)
* [一致性](#一致性)
* [可用性](#可用性)
@@ -28,15 +29,15 @@
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
* [多个 Candidate 竞选](#多个-candidate-竞选)
* [日志复制](#日志复制)
* [七、RPC](#七rpc)
* [七、RPC](#七rpc) 🔥
* [调用过程](#调用过程)
* [服务发现](#服务发现)
* [RPC 与 HTTP](#rpc-与-http)
* [超时与重试](#超时与重试)
* [八、分布式 ID](#八分布式-id)
* [八、分布式 ID](#八分布式-id) 🔥
* [雪花算法](#雪花算法)
* [九、幂等](#九幂等)
* [十、分布式定时任务](#十分布式定时任务)
* [九、幂等](#九幂等) 🔥🔥
* [十、分布式定时任务](#十分布式定时任务) 🔥
* [只执行一次](#只执行一次)
* [调度中心](#调度中心)
* [分片](#分片)
@@ -54,7 +55,7 @@
<!-- GFM-TOC -->
## 一、分布式锁
## 一、分布式锁 🔥🔥
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
@@ -199,12 +200,14 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
锁与本地事务一起使用时,锁要包住事务,在事务提交之后再释放,否则后来的请求拿到锁时还看不到前一个请求的修改,见 [Spring 中用自定义注解加锁](Spring.md#动态代理)。
## 二、分布式事务
## 二、分布式事务 🔥
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
严格满足 ACID 的代价很高,参与者要一直持有锁,等所有节点都确认。下面的做法中,2PC 让所有参与者一起提交或一起回滚;TCC、Saga 和本地消息表则让各个参与者先提交自己的本地事务,中间状态对其它事务可见,只保证最终一致,也就是下文 [BASE](#四base) 的思路。
分布式锁和分布式事务区别:
- 锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确;
@@ -270,7 +273,7 @@ TCC(Try-Confirm-Cancel)是在业务层面实现的两阶段提交,每个
如果所有参与者的 Try 都执行成功,就执行 Confirm,否则执行 Cancel。Confirm 和 Cancel 失败后都需要重试,因此必须实现幂等。
相比于 2PC,TCC 不需要长时间持有数据库的锁,性能更好,但是对业务的侵入性较强。
相比于 2PC,TCC 不需要长时间持有数据库的锁,性能更好,但是对业务的侵入性较强。它适合扣库存、扣余额这类一致性要求高、能提供预留和取消接口的资源操作。
实现时还需要处理两个问题:
@@ -289,20 +292,32 @@ Saga 没有预留资源的 Try 阶段,中间状态对其它事务可见,因
### 本地消息表
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
本地消息表与业务数据表放在同一个数据库中,用本地事务保证业务数据和消息一起写入,再通过消息队列把消息送到另一个服务。以下单后给用户加积分为例:
1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除(或者标记为已发送),否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
1. 订单服务在同一个本地事务中插入订单 1024 和一条待发送的消息,两行要么都写入,要么都不写入;
2. 定时任务扫描待发送的消息,转发到消息队列,发送成功后把消息标记为已发送(或者删除),失败就在下次扫描时重新转发;
3. 积分服务从消息队列中读取消息,给用户加积分。
<div align="center"> <img src="pics/local-message-table.png" width="760px"/> </div><br>
由于重新转发会导致消息被重复投递,因此另一方执行的操作需要实现幂等。并且第一方的事务已经提交,无法回滚,所以如果另一方执行失败,只能不断重试或者人工介入处理。
由于重新转发会导致消息被重复投递,积分服务要按订单号去重,实现幂等。订单服务的事务已经提交,无法回滚,积分服务执行失败时只能不断重试或者人工介入处理。所以它适合发券、加积分这类能接受最终一致、失败后可以重试的后续操作。
RocketMQ 的事务消息把本地消息表的工作交给了消息队列:生产者先发送一条对消费者不可见的半消息,再执行本地事务,然后根据执行结果通知 Broker 提交或者回滚这条消息。如果 Broker 长时间没有收到通知,会主动回查生产者的本地事务状态。
<div align="center"> <img src="pics/rocketmq-transaction.png" width="600px"/> </div><br>
### 几种做法的对比
| 做法 | 一致性 | 资源的锁定 | 业务改造 | 适合的场景 |
| --- | --- | --- | --- | --- |
| 2PC(XA) | 所有参与者一起提交或回滚 | 从准备到提交一直持有数据库的锁 | 少,由数据库和协调者完成 | 参与者少、并发不高 |
| Seata AT | 最终一致,回滚靠反向 SQL,默认可能读到全局事务未提交的修改 | 本地事务立即提交,修改过的行持有全局锁直到第二阶段 | 几乎没有 | 普通的增删改,希望少改代码 |
| TCC | 最终一致,Try 阶段预留资源 | 只锁定预留的资源,不持有数据库的锁 | 每个操作写 Try、Confirm、Cancel 三个接口 | 扣库存、扣余额等资源操作 |
| Saga | 最终一致,没有隔离性 | 不锁定 | 每一步写补偿操作 | 流程长、跨多个服务 |
| 本地消息表、事务消息 | 最终一致,只能重试,不能回滚 | 不锁定 | 写消息和重试,消费方实现幂等 | 发券、加积分等后续操作 |
同一个业务中常把几种做法组合使用,例如下单时用 TCC 冻结库存,支付成功后用事务消息通知积分服务加积分。
## 三、CAP
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance)。常见的“三选二”说法有误导性,因为网络分区是无法避免的故障,不是可以选择的选项。
@@ -368,6 +383,14 @@ BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。“最终”没有时间上限,因此通常还会提供读己之写(用户能读到自己刚写入的数据)和单调读(不会读到比上次更旧的数据)等保证来减少不一致带来的困扰。
按保证的强弱,常说的一致性分为三类:
- 强一致性:即上文的线性一致性,写入成功之后,所有读请求都能读到新值;
- 弱一致性:写入之后不保证马上能读到新值;
- 最终一致性:弱一致性的一种,保证不再有新的写入时,所有副本最终都会相同。
例如订单 1024 支付成功后,主库中已经是“已支付”,从库在复制完成之前仍返回“待支付”。强一致的系统不允许出现这种情况,最终一致的系统允许,但复制完成后必须读到“已支付”。强一致和最终一致之间还有因果一致性等模型,它只保证有因果关系的写入按先后被看到,例如看到订单 1024“已发货”的用户,一定也能看到它“已支付”。
ACID 要求每个事务结束时数据都满足约束,并且事务之间相互隔离,通常运用在传统的数据库系统上。而 BASE 允许数据在一段时间内处于中间状态,只要求最终一致,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
@@ -491,7 +514,7 @@ Raft 与 Paxos 达成的结果是等价的,区别在于对 Leader 的依赖程
ZooKeeper 使用的 ZAB 协议同样以 Leader 为中心,纪元编号(epoch)的作用相当于 Raft 的任期号。
## 七、RPC
## 七、RPC 🔥
远程过程调用(Remote Procedure Call, RPC)指的是像调用本地方法一样调用另一台机器上的服务。例如订单服务调用库存服务扣减库存:
@@ -592,7 +615,7 @@ gRPC 选择 HTTP/2 是因为它的 [多路复用](HTTP.md#二进制分帧层):
- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重,否则重试会扣减两次;
- 调用链较长时,下游的超时时间应该比上游短。gRPC 会把截止时间(deadline)随请求传给服务方,超时后调用方收到 DEADLINE_EXCEEDED,服务方也可以检查调用是否已经超时,从而停止处理。
## 八、分布式 ID
## 八、分布式 ID 🔥
单库时用数据库的自增主键就能生成 ID,分库分表之后,各个库的自增序列会重复,需要一个全局唯一的 ID。一般有以下要求:
@@ -646,7 +669,7 @@ public synchronized long nextId() {
- 机器 ID:每个实例的机器 ID 不能相同,可以在启动时从 ZooKeeper、数据库或配置中心领取;
- 时钟回拨:服务器校时可能让时钟倒退,回拨较小时可以等待时钟追上,较大时报错,或者像美团的 Leaf 那样在启动时检查本机时间与其它节点是否一致。
## 九、幂等
## 九、幂等 🔥🔥
幂等指的是同一个操作执行一次和执行多次的效果相同。分布式系统中重复的请求很常见:调用超时后重试、用户重复点击、消息队列重复投递,都会让同一个操作执行多次。查询和删除天然幂等,新增和扣减不是,例如支付回调被调用两次,订单就可能被处理两次。HTTP 规范中各个方法是否幂等,见 [HTTP 的幂等性](HTTP.md#幂等性)。
@@ -665,13 +688,15 @@ public synchronized long nextId() {
- 服务端在订单表的 request_id 上建唯一索引,插入订单时重复的请求报错 1062,返回第一次的结果。同一个 request_id 并发提交 10 次,先查后插的写法 5 轮中每轮都创建了 2~3 个订单,唯一索引每轮只创建 1 个(MySQL 8.0 实测,见[附录 A4](#a4-重复提交与唯一索引));
- 前端把按钮置灰、网关限流可以减少重复请求,但请求仍可能来自重试、脚本或者网络重发,只能作为辅助,最终要靠数据库的约束兜底。
<div align="center"> <img src="pics/idempotent-request-id.png" width="760px"/> </div><br>
没有客户端参与的场景,幂等键用业务上的唯一标识。例如支付回调用支付平台的交易号,再配合上面的状态机:重复的回调影响 0 行时,同样要返回成功,否则支付平台会继续重试。
分布式锁不能代替幂等。锁只保证同一时刻只有一个请求在处理,第一个请求完成并释放锁之后,重试的请求拿到锁仍会再执行一次。锁的作用是挡住并发的重复请求,减轻数据库的冲突,是否已经处理过还是要靠唯一索引或者状态机判断。
用 Token 实现时,Token 的过期时间按用户在页面上停留的时长设置,例如 30 分钟。DEL 返回 1 之后处理失败时,这个 Token 已经删除,客户端要重新获取 Token 再提交。
用 Token 实现时,Token 的过期时间按用户在页面上停留的时长设置,例如 30 分钟。DEL 返回 1 之后处理失败时,这个 Token 已经删除,客户端要重新获取 Token 再提交。需要注意的是,DEL 和之后的业务处理不是一个原子操作:处理成功但响应丢失时,客户端分不清是否已经处理,换一个新 Token 重试就会再执行一次。所以 Token 只能挡住页面上的重复点击,最终的去重仍要靠订单号这类业务唯一约束。
## 十、分布式定时任务
## 十、分布式定时任务 🔥
定时任务指的是按时间规则自动执行的任务,例如每月 1 日凌晨 2 点给所有用户发送上月的账单。只有一个实例时,用 Spring 的 `@Scheduled(cron = "0 0 2 1 * ?")` 就能实现。
@@ -937,6 +962,8 @@ NOWAIT:0.1 ms 返回 (3572, 'Statement aborted because lock(s) could not be ac
- [Seata 是什么](https://seata.apache.org/zh-cn/docs/overview/what-is-seata/)
- [Seata 术语](https://seata.apache.org/zh-cn/docs/overview/terminology/)
- [Seata XA 模式](https://seata.apache.org/zh-cn/docs/user/mode/xa/)
- [Seata AT 模式](https://seata.apache.org/zh-cn/docs/dev/mode/at-mode/)
- [Eventually Consistent - Revisited](https://www.allthingsdistributed.com/2008/12/eventually_consistent.html)
- [gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)
- [Dubbo 协议详解](https://cn.dubbo.apache.org/zh-cn/blog/2018/10/05/dubbo-%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3/)
- [深度剖析 Seata TCC 模式](https://seata.apache.org/zh-cn/blog/seata-tcc/)