mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 02:53:42 +00:00
Update 分布式.md
补充 Redisson 看门狗与可重入、Seata 的角色与模式、秒杀场景下分布式锁的替代、gRPC 与 RPC 的帧格式、幂等键与防重复下单、ZAB 与 Raft 的对比,增加配图和实测附录。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
+121
-7
@@ -37,6 +37,10 @@
|
||||
* [雪花算法](#雪花算法)
|
||||
* [九、幂等](#九幂等)
|
||||
* [附录:实测](#附录实测)
|
||||
* [A1 自增主键与 UUID 主键](#a1-自增主键与-uuid-主键)
|
||||
* [A2 雪花算法](#a2-雪花算法)
|
||||
* [A3 Redisson 的可重入与看门狗](#a3-redisson-的可重入与看门狗)
|
||||
* [A4 重复提交与唯一索引](#a4-重复提交与唯一索引)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -75,7 +79,7 @@ SET lock_key unique_value NX PX 30000
|
||||
还需要注意以下几点:
|
||||
|
||||
- 释放锁:value 设置为客户端的唯一标识,释放锁时先判断 value 是否为自己设置的再删除,避免误删其它客户端的锁,判断和删除需要使用 Lua 脚本保证原子性;
|
||||
- 锁续期:如果业务执行时间超过了过期时间,锁会被提前释放,可以使用后台线程定时延长过期时间,例如 Redisson 的看门狗机制默认每 10 秒把过期时间重置为 30 秒;
|
||||
- 锁续期:如果业务执行时间超过了过期时间,锁会被提前释放,可以使用后台线程定时延长过期时间,例如下文 Redisson 的看门狗;
|
||||
- 主从切换:Redis 的主从复制是异步的,如果主节点加锁后还未同步到从节点就宕机,从节点晋升为主节点之后锁就丢失了。
|
||||
|
||||
```lua
|
||||
@@ -88,6 +92,18 @@ end
|
||||
|
||||
Redis 8.4 新增了 `DELEX lock_key IFEQ unique_value` 指令,可以直接原子地完成比较并删除的操作,不再需要 Lua 脚本。
|
||||
|
||||
实际项目中通常使用 Redisson 的 RLock,它在 SET NX 的基础上解决了可重入、等待和续期三个问题。以锁 lock:order:1024 为例(Redisson 4.8、Redis 7.4 实测,见[附录 A3](#a3-redisson-的可重入与看门狗)):
|
||||
|
||||
- 可重入:锁是一个哈希,字段是“客户端 UUID:线程 ID”,值是加锁次数。加锁的 Lua 脚本在键不存在、或者字段就是自己时,把次数加 1 并设置过期时间,否则返回锁的剩余过期时间。同一个线程加锁两次后 HGETALL 返回 [uuid:1, 2],解锁一次变为 1,再解锁一次才删除这个键;
|
||||
- 等待:加锁失败的线程订阅这把锁对应的频道,持有者释放锁时向频道发布一条消息,等待的线程收到后再去加锁,而不是不停地轮询;
|
||||
- 看门狗:调用 lock() 时不指定过期时间,Redisson 先把过期时间设为 lockWatchdogTimeout(默认 30 秒),再启动一个定时任务,每 10 秒(30 秒的 1/3)检查一次,锁仍是自己的就把过期时间重置为 30 秒,解锁时取消这个任务。
|
||||
|
||||
<div align="center"> <img src="pics/redisson-watchdog.png" width="720px"/> </div><br>
|
||||
|
||||
看门狗运行在持有锁的进程中,只在这个进程存活时续期。进程崩溃后没有人续期,锁最多 30 秒后自动过期,其它客户端就能拿到锁。反过来,如果持有锁的线程卡住(例如等待一个没有设置超时的远程调用)而进程还活着,看门狗会一直续期,其它客户端一直拿不到锁,所以锁内的操作都要设置超时。
|
||||
|
||||
调用 lock(10, TimeUnit.SECONDS) 指定了过期时间时,Redisson 不启动看门狗,10 秒后锁被自动删除,即使业务还没有执行完。需要注意的是,Redisson 的锁仍然保存在一个 Redis 主节点上,主从切换时丢锁的问题依然存在。
|
||||
|
||||
### Redis 的 RedLock 算法
|
||||
|
||||
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
|
||||
@@ -157,6 +173,12 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
|
||||
|
||||
实际使用中一般直接采用 Redisson、Curator 等成熟的客户端实现。
|
||||
|
||||
锁的粒度应该和要保护的资源一致:限制一人一单就锁 lock:order:user:1024,扣减某个商品的库存就锁 lock:stock:10086,不要让整个下单接口共用一把锁,否则不相关的请求也要排队。
|
||||
|
||||
所有请求都争抢同一个资源时,例如秒杀同一件商品,分布式锁会让请求排队串行执行,每个请求还要多两次访问 Redis,吞吐量受限,锁的键也成了热点。判断库存并扣减这类可以在一次原子操作中完成的逻辑,更适合直接用 Redis 的 Lua 脚本或者数据库的条件更新,见 [秒杀系统](系统设计基础.md#六秒杀系统)。
|
||||
|
||||
锁与本地事务一起使用时,锁要包住事务,在事务提交之后再释放,否则后来的请求拿到锁时还看不到前一个请求的修改,见 [Spring 中用自定义注解加锁](Spring.md#动态代理)。
|
||||
|
||||
## 二、分布式事务
|
||||
|
||||
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
|
||||
@@ -168,6 +190,12 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
|
||||
- 锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确;
|
||||
- 而事务问题的关键则在于事务涉及的一系列操作需要满足 ACID 特性,例如要满足原子性操作则需要这些操作要么都执行,要么都不执行。
|
||||
|
||||
下面介绍的几种做法,常用的分布式事务框架 Seata 都提供了实现。Seata 中有三个角色:事务协调者(Transaction Coordinator, TC)是单独部署的服务,记录全局事务和各个分支事务的状态,驱动它们提交或回滚;事务管理器(Transaction Manager, TM)在发起全局事务的服务中,负责开始、提交或回滚全局事务;资源管理器(Resource Manager, RM)在每个参与的服务中,负责注册分支事务、报告状态并执行提交或回滚。它提供四种模式:
|
||||
|
||||
- XA:即下文的 2PC,由数据库执行 XA PREPARE,锁一直持有到第二阶段;
|
||||
- AT:第一阶段直接提交本地事务,同时记录修改前后的数据,失败时按记录生成反向的 SQL 回滚,对业务代码没有侵入,过程见 [微服务中的 Seata](微服务.md#七服务保护与分布式事务);
|
||||
- TCC、Saga:即下文的两种做法,需要业务自己实现预留、确认、取消或补偿的接口。
|
||||
|
||||
### 2PC
|
||||
|
||||
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
|
||||
@@ -210,6 +238,8 @@ Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节
|
||||
|
||||
三阶段提交(3PC)在 2PC 的基础上增加了一个询问阶段(CanCommit),并且在参与者端也引入了超时机制:参与者收到预提交(PreCommit)之后,如果等待最终通知超时,就默认提交,从而减少了阻塞。但是它假设网络延迟和节点的响应时间都有上界,而现实系统中存在无界的网络延迟和进程暂停,超时并不能说明对方已经失效,因此仍然可能出现数据不一致。实践中更常见的做法是用共识算法把协调者的决定复制到多个节点上,避免单点。
|
||||
|
||||
两阶段提交也用在单机的数据库内部:MySQL 用它保证 redo log 和 binlog 一致,见 [MySQL 的两阶段提交](MySQL.md#两阶段提交)。
|
||||
|
||||
### TCC
|
||||
|
||||
TCC(Try-Confirm-Cancel)是在业务层面实现的两阶段提交,每个操作都需要实现三个接口:
|
||||
@@ -397,7 +427,7 @@ Raft 也是分布式共识算法,相比于 Paxos 更容易理解和实现。
|
||||
|
||||
有三种节点:Follower、Candidate 和 Leader。Raft 把时间划分为多个任期(Term),任期号单调递增,每个任期从一次选举开始,并且每个任期内最多只有一个 Leader。任期号的作用和 Paxos 中的提议序号相同,发生冲突时任期号大的一方说了算。
|
||||
|
||||
Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,将任期号加 1 并给自己投一票,进入竞选阶段。
|
||||
Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,论文中的例子是 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,将任期号加 1 并给自己投一票,进入竞选阶段。
|
||||
|
||||
- 系统刚启动时只有 Follower 没有 Leader。Node A 的随机竞选超时最短,到期时没收到 Leader 发来的心跳包,因此进入竞选阶段,发送投票请求给其它所有节点。
|
||||
|
||||
@@ -432,6 +462,13 @@ Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了
|
||||
|
||||
读请求则要另外处理:旧 Leader 如果直接用本地数据回答读请求,可能返回旧数据,所以 Raft 要求 Leader 在处理只读请求前,先与大多数节点交换一次心跳,确认自己仍是 Leader。另外,新 Leader 刚上任时还不知道哪些日志已经提交,要先提交一条当前任期的空日志,之后才能处理只读请求。
|
||||
|
||||
Raft 与 Paxos 达成的结果是等价的,区别在于对 Leader 的依赖程度:
|
||||
|
||||
- Paxos 中任何一个 Proposer 都可以发起提议,选出一个主 Proposer 只是性能上的优化(也用来避免上文的活锁)。Multi-Paxos 中日志的每个位置各自独立地确定,新的主节点还要先找回自己缺少的日志;
|
||||
- Raft 中选主是协议的一部分,只有 Leader 能追加日志,日志只从 Leader 流向 Follower,投票规则又保证只有日志最新的节点能当选,新 Leader 不需要从其它节点补日志,因此更容易理解和实现。代价是所有写入都要经过 Leader,选举期间不能写入。
|
||||
|
||||
ZooKeeper 使用的 ZAB 协议同样以 Leader 为中心,纪元编号(epoch)的作用相当于 Raft 的任期号。
|
||||
|
||||
## 七、RPC
|
||||
|
||||
远程过程调用(Remote Procedure Call, RPC)指的是像调用本地方法一样调用另一台机器上的服务。例如订单服务调用库存服务扣减库存:
|
||||
@@ -497,6 +534,8 @@ inventoryService.deduct(10086, 2); // 实际执行的是服务方的 Inventory
|
||||
|
||||
这段代码每次调用都新建连接,使用 Java 自带的序列化,也没有超时处理。Dubbo、gRPC 等框架在此基础上实现了连接复用、高效的序列化、服务发现、负载均衡、超时重试和熔断,设计一个 RPC 框架时也是从这几个模块考虑。
|
||||
|
||||
复用连接之后,要在 TCP 的字节流中分出一个个请求。TCP 不保留消息的边界,所以协议都在消息前写明长度。例如 Dubbo 协议的首部固定为 16 字节:2 字节魔数 0xdabb、1 字节标志(请求还是响应、是否需要返回值、序列化方式)、1 字节响应状态、8 字节请求 ID、4 字节正文长度。接收方先读 16 字节的首部,再按长度读出正文,用请求 ID 找到对应的调用。
|
||||
|
||||
### 服务发现
|
||||
|
||||
服务的实例会扩容、缩容或者宕机,地址不能写死在调用方。注册中心(例如 Nacos、ZooKeeper)保存每个服务当前可用的实例:
|
||||
@@ -507,6 +546,8 @@ inventoryService.deduct(10086, 2); // 实际执行的是服务方的 Inventory
|
||||
|
||||
因此注册中心全部宕机时,调用方仍然可以按缓存的地址继续调用,只是新上线的实例无法被发现。某个实例宕机后、被注册中心删除之前,发往它的请求会失败,需要调用方重试其它实例。
|
||||
|
||||
Nacos、Eureka、ZooKeeper 作为注册中心的区别,以及注册中心与客户端负载均衡在 Spring Cloud 中如何配合,见 [微服务](微服务.md#三注册中心)。
|
||||
|
||||
### RPC 与 HTTP
|
||||
|
||||
HTTP 是一种协议,RPC 是一种调用方式,两者不是同一层面的概念:RPC 框架可以在 TCP 上使用自定义的协议,例如 Dubbo 协议;也可以基于 HTTP,例如 gRPC 基于 HTTP/2,Spring Cloud 的 OpenFeign 用动态代理把接口调用转换成 HTTP 请求。
|
||||
@@ -520,6 +561,8 @@ HTTP 是一种协议,RPC 是一种调用方式,两者不是同一层面的
|
||||
|
||||
HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用,报文可读,便于调试,适合对外开放的接口。
|
||||
|
||||
gRPC 选择 HTTP/2 是因为它的 [多路复用](HTTP.md#二进制分帧层):每次调用是一个流(Stream),请求路径为 /包名.服务名/方法名,例如 /inventory.InventoryService/Deduct;多个调用的帧在同一个连接上交错发送,用流 ID 区分,不需要自己设计请求 ID。参数编码成 Protobuf 后,前面加 1 字节的压缩标志和 4 字节的长度,放在 DATA 帧中。上面的参数在 DATA 帧中就是 00 00 00 00 05 08 E6 4E 10 02,前 5 个字节表示不压缩、长度为 5。
|
||||
|
||||
### 超时与重试
|
||||
|
||||
- 每次调用都要设置超时时间,否则下游变慢时,调用方的线程会一直等待,最终被耗尽;
|
||||
@@ -545,7 +588,7 @@ HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用
|
||||
| Redis INCR | 用 INCR 得到递增的整数 | 依赖 Redis 的可用性和持久化,宕机后可能重复 |
|
||||
| 雪花算法 | 本地用时间戳、机器 ID 和序号拼成 64 位整数 | 依赖时钟,需要分配机器 ID |
|
||||
|
||||
例如分别以自增整数和 UUID() 为主键插入 100 万行,自增主键用时 3.4 秒、数据 133 MB,UUID 主键用时 5.1 秒、数据 164 MB(MySQL 8.0 实测,见[附录:实测](#附录实测))。这是因为 UUID 的值是无序的,每次都插入到 B+ 树中间的随机位置,频繁的页分裂让页的填充率变低,二级索引中保存的主键也更长。
|
||||
例如分别以自增整数和 UUID() 为主键插入 100 万行,自增主键用时 3.4 秒、数据 133 MB,UUID 主键用时 5.1 秒、数据 164 MB(MySQL 8.0 实测,见[附录 A1](#a1-自增主键与-uuid-主键))。这是因为 UUID 的值是无序的,每次都插入到 B+ 树中间的随机位置,频繁的页分裂让页的填充率变低,二级索引中保存的主键也更长。
|
||||
|
||||
### 雪花算法
|
||||
|
||||
@@ -572,7 +615,7 @@ public synchronized long nextId() {
|
||||
}
|
||||
```
|
||||
|
||||
用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有约 240 万个不重复(JDK 21 实测,见[附录:实测](#附录实测))。
|
||||
用 8 个线程共生成 400 万个 ID,耗时 1.2 秒,全部不重复,每个线程得到的 ID 都是递增的;去掉“序号用完就等到下一毫秒”这一步,400 万个 ID 中只有约 240 万个不重复(JDK 21 实测,见[附录 A2](#a2-雪花算法))。
|
||||
|
||||
需要注意的是,同一个实例生成的 ID 是递增的,但不同实例的时钟之间存在偏差,两个实例生成的 ID 只能大致反映生成的先后顺序。
|
||||
|
||||
@@ -583,7 +626,7 @@ public synchronized long nextId() {
|
||||
|
||||
## 九、幂等
|
||||
|
||||
幂等指的是同一个操作执行一次和执行多次的效果相同。分布式系统中重复的请求很常见:调用超时后重试、用户重复点击、消息队列重复投递,都会让同一个操作执行多次。查询和删除天然幂等,新增和扣减不是,例如支付回调被调用两次,订单就可能被处理两次。
|
||||
幂等指的是同一个操作执行一次和执行多次的效果相同。分布式系统中重复的请求很常见:调用超时后重试、用户重复点击、消息队列重复投递,都会让同一个操作执行多次。查询和删除天然幂等,新增和扣减不是,例如支付回调被调用两次,订单就可能被处理两次。HTTP 规范中各个方法是否幂等,见 [HTTP 的幂等性](HTTP.md#幂等性)。
|
||||
|
||||
实现方法:
|
||||
|
||||
@@ -594,11 +637,25 @@ public synchronized long nextId() {
|
||||
|
||||
需要注意的是,“先查询是否处理过、再处理”分成两步时,两个并发的重复请求可能同时查到“未处理”。判断和处理必须是原子的,要么依靠数据库的唯一索引或条件更新,要么把判断和写入放在同一个 Lua 脚本中。
|
||||
|
||||
实现幂等首先要定义什么算同一个请求,也就是幂等键怎么来。以用户狂点“提交订单”为例:
|
||||
|
||||
- 客户端在打开确认页时生成一个 request_id,之后的每次点击和重试都带着它,下单成功或者用户修改了订单内容之后才换一个新的。如果每次点击都生成一个新的 ID,服务端就无法识别重复;
|
||||
- 服务端在订单表的 request_id 上建唯一索引,插入订单时重复的请求报错 1062,返回第一次的结果。同一个 request_id 并发提交 10 次,先查后插的写法 5 轮中每轮都创建了 2~3 个订单,唯一索引每轮只创建 1 个(MySQL 8.0 实测,见[附录 A4](#a4-重复提交与唯一索引));
|
||||
- 前端把按钮置灰、网关限流可以减少重复请求,但请求仍可能来自重试、脚本或者网络重发,只能作为辅助,最终要靠数据库的约束兜底。
|
||||
|
||||
没有客户端参与的场景,幂等键用业务上的唯一标识。例如支付回调用支付平台的交易号,再配合上面的状态机:重复的回调影响 0 行时,同样要返回成功,否则支付平台会继续重试。
|
||||
|
||||
分布式锁不能代替幂等。锁只保证同一时刻只有一个请求在处理,第一个请求完成并释放锁之后,重试的请求拿到锁仍会再执行一次。锁的作用是挡住并发的重复请求,减轻数据库的冲突,是否已经处理过还是要靠唯一索引或者状态机判断。
|
||||
|
||||
用 Token 实现时,Token 的过期时间按用户在页面上停留的时长设置,例如 30 分钟。DEL 返回 1 之后处理失败时,这个 Token 已经删除,客户端要重新获取 Token 再提交。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
环境为 Apple M1 Pro、macOS,MySQL 8.0.44,JDK 21。
|
||||
|
||||
自增主键与 UUID 主键,每张表插入 100 万行,v 列为 100 个字符:
|
||||
### A1 自增主键与 UUID 主键
|
||||
|
||||
每张表插入 100 万行,v 列为 100 个字符:
|
||||
|
||||
```sql
|
||||
create table t_auto (id bigint primary key auto_increment, v char(100)) engine=innodb;
|
||||
@@ -615,13 +672,64 @@ t_auto 133 7
|
||||
t_uuid 164 7
|
||||
```
|
||||
|
||||
雪花算法:正文中的 nextId() 补齐字段后,8 个线程各生成 50 万个 ID,检查是否重复、每个线程内是否递增;第二行是去掉“序号用完就等到下一毫秒”之后的结果:
|
||||
### A2 雪花算法
|
||||
|
||||
正文中的 nextId() 补齐字段后,8 个线程各生成 50 万个 ID,检查是否重复、每个线程内是否递增;第二行是去掉“序号用完就等到下一毫秒”之后的结果:
|
||||
|
||||
```text
|
||||
generated 4000000 unique=4000000 increasingPerThread=true ms=1166
|
||||
generated 4000000 unique=2404324 increasingPerThread=false ms=712
|
||||
```
|
||||
|
||||
### A3 Redisson 的可重入与看门狗
|
||||
|
||||
Redisson 4.8.0,Redis 7.4.1。三个程序分别加锁 lock:order:1024,用 redis-cli 查看哈希的内容和 PTTL:
|
||||
|
||||
```java
|
||||
RLock lock = redisson.getLock("lock:order:1024");
|
||||
// 程序一:lock() 两次,持有 30 秒,再 unlock() 两次
|
||||
// 程序二:lock(10, TimeUnit.SECONDS),之后不解锁
|
||||
// 程序三:lock() 之后立即 Runtime.getRuntime().halt(1),模拟进程崩溃
|
||||
```
|
||||
|
||||
```text
|
||||
== 程序一
|
||||
0.0s lock() HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 1] PTTL=29979
|
||||
0.0s 再次 lock() HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=29991
|
||||
6.1s 持有中 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=23973
|
||||
12.1s 持有中 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=27977
|
||||
18.1s 持有中 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=21961
|
||||
24.1s 持有中 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=25955
|
||||
30.1s 持有中 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 2] PTTL=29966
|
||||
30.1s unlock() 一次 HGETALL=[0fb9b3a2-f60d-4d48-b081-ccbdacb0b4b6:1 1] PTTL=29987
|
||||
30.2s unlock() 两次 HGETALL=[] PTTL=-2
|
||||
== 程序二
|
||||
0.0s lock(10s) HGETALL=[f6be60e4-44c3-4086-bc03-2cd44ddd0459:1 1] PTTL=9988
|
||||
4.0s 持有中 HGETALL=[f6be60e4-44c3-4086-bc03-2cd44ddd0459:1 1] PTTL=5971
|
||||
8.1s 持有中 HGETALL=[f6be60e4-44c3-4086-bc03-2cd44ddd0459:1 1] PTTL=1950
|
||||
12.1s 持有中 HGETALL=[] PTTL=-2
|
||||
== 程序三
|
||||
0.0s lock(),随后 halt PTTL=29985
|
||||
崩溃后 25s PTTL=4480
|
||||
崩溃后 30s PTTL=-2
|
||||
(另一个客户端 tryLock(0, SECONDS) 返回 true)
|
||||
```
|
||||
|
||||
配图中的曲线是另外运行一次、每 0.5 秒采样一次 PTTL 得到的。
|
||||
|
||||
### A4 重复提交与唯一索引
|
||||
|
||||
MySQL 8.0.44,Python 3 + pymysql。两张订单表结构相同,只是 t3_orders_uk 在 request_id 上有唯一索引。每轮用 10 个线程同时提交同一个 request_id,先查后插的写法先 SELECT,没有记录再 INSERT;唯一索引的写法直接 INSERT,捕获 1062 错误:
|
||||
|
||||
```text
|
||||
先查后插 第 1 轮:同一个 request_id 并发提交 10 次,创建订单 2 个
|
||||
先查后插 第 2 轮:同一个 request_id 并发提交 10 次,创建订单 2 个
|
||||
先查后插 第 3 轮:同一个 request_id 并发提交 10 次,创建订单 3 个
|
||||
先查后插 第 4 轮:同一个 request_id 并发提交 10 次,创建订单 2 个
|
||||
先查后插 第 5 轮:同一个 request_id 并发提交 10 次,创建订单 2 个
|
||||
唯一索引 第 1~5 轮:同一个 request_id 并发提交 10 次,创建订单 1 个,其余 9 次报错 1062
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/)
|
||||
@@ -636,6 +744,12 @@ generated 4000000 unique=2404324 increasingPerThread=false ms=712
|
||||
- [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/)
|
||||
- [Redisson 源码:RedissonLock](https://github.com/redisson/redisson/blob/master/redisson/src/main/java/org/redisson/RedissonLock.java)
|
||||
- [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/)
|
||||
- [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/)
|
||||
- [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/)
|
||||
|
||||
Reference in New Issue
Block a user