auto commit
This commit is contained in:
289
notes/分布式问题分析.md
289
notes/分布式问题分析.md
@ -1,83 +1,175 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、谈谈业务中使用分布式的场景](#一谈谈业务中使用分布式的场景)
|
||||
* [二、分布式事务](#二分布式事务)
|
||||
* [产生原因](#产生原因)
|
||||
* [应用场景](#应用场景)
|
||||
* [解决方案](#解决方案)
|
||||
* [三、负载均衡的算法与实现](#三负载均衡的算法与实现)
|
||||
* [一、分布式事务](#一分布式事务)
|
||||
* [两阶段提交协议](#两阶段提交协议)
|
||||
* [本地消息](#本地消息)
|
||||
* [二、分布式锁](#二分布式锁)
|
||||
* [原理](#原理)
|
||||
* [实现](#实现)
|
||||
* [三、分布式 Session](#三分布式-session)
|
||||
* [四、负载均衡](#四负载均衡)
|
||||
* [算法](#算法)
|
||||
* [实现](#实现)
|
||||
* [四、分布式锁](#四分布式锁)
|
||||
* [使用场景](#使用场景)
|
||||
* [实现方式](#实现方式)
|
||||
* [五、分布式 Session](#五分布式-session)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、谈谈业务中使用分布式的场景
|
||||
# 一、分布式事务
|
||||
|
||||
分布式主要是为了提供可扩展性以及高可用性,业务中使用分布式的场景主要有分布式存储以及分布式计算。
|
||||
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就需要涉及分布式事务。
|
||||
|
||||
分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份(高可用性)。
|
||||
## 两阶段提交协议
|
||||
|
||||
至于分布式计算,就是将一个大的计算任务分解成小任务分配到多个节点上去执行,再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算最好的例子。
|
||||
Two-phase Commit(2PC)。
|
||||
|
||||
# 二、分布式事务
|
||||
两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。
|
||||
|
||||
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
|
||||
### 1. 运行过程
|
||||
|
||||
## 产生原因
|
||||
① 准备阶段:协调者询问参与者事务是否执行成功;
|
||||
|
||||
- 数据库分库分表;
|
||||
- SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。
|
||||
<div align="center"> <img src="../pics//c8dbff58-d981-48be-8c1c-caa6c2738791.jpg"/> </div><br>
|
||||
|
||||
## 应用场景
|
||||
② 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
|
||||
|
||||
- 下单:减少库存、更新订单状态。库存和订单如果不在同一个数据库,就涉及分布式事务。
|
||||
- 支付:买家账户扣款、卖家账户入账。买家和卖家账户信息如果不在同一个数据库,就涉及分布式事务。
|
||||
<div align="center"> <img src="../pics//aa844ff0-cd16-4478-b415-da071b615a17.jpg"/> </div><br>
|
||||
|
||||
## 解决方案
|
||||
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
|
||||
|
||||
### 1. 两阶段提交协议
|
||||
### 2. 分析
|
||||
|
||||
> [两阶段提交](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE.md#%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E5%8D%8F%E8%AE%AE)
|
||||
2PC 可以保证强一致性,但是因为在准备阶段协调者需要等待所有参与者的结果才能进入提交阶段,因此可用性差。
|
||||
|
||||
两阶段提交协议可以很好地解决分布式事务问题。它可以使用 XA 来实现,XA 包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。
|
||||
### 3. 存在的问题
|
||||
|
||||
### 2. 消息中间件
|
||||
- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
|
||||
- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
|
||||
|
||||
消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转入余额宝操作,支付宝系统执行减少余额操作之后向消息系统发送一个消息,余额宝系统订阅这条消息然后进行增加余额宝操作。
|
||||
### 4. XA 协议
|
||||
|
||||
#### 2.1 消息处理模型
|
||||
XA 协议是多数数据库的 2PC 协议的实现,包含了事务管理器和本地资源管理器。
|
||||
|
||||
**(一)消息队列**
|
||||
## 本地消息
|
||||
|
||||
<div align="center"> <img src="../pics//96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg" width="700"/> </div><br>
|
||||
### 1. 原理
|
||||
|
||||
**(二)发布/订阅**
|
||||
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。
|
||||
|
||||
<div align="center"> <img src="../pics//654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg" width="700"/> </div><br>
|
||||
1. 在分布式事务操作的一方,它完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
|
||||
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
|
||||
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
|
||||
|
||||
<div align="center"> <img src="../pics//e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg"/> </div><br>
|
||||
|
||||
#### 2.2 消息的可靠性
|
||||
### 2. 分析
|
||||
|
||||
**(一)发送端的可靠性**
|
||||
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
|
||||
|
||||
发送端完成操作后一定能将消息成功发送到消息系统。
|
||||
# 二、分布式锁
|
||||
|
||||
实现方法:在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
|
||||
可以使用 Java 提供的内置锁来实现进程同步:由 JVM 实现的 synchronized 和 JDK 提供的 Lock。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁来同步。
|
||||
|
||||
**(二)接收端的可靠性**
|
||||
## 原理
|
||||
|
||||
接收端能够从消息中间件成功消费一次消息。
|
||||
锁可以有阻塞锁和乐观锁两种实现方式,这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1,也就是锁定状态。
|
||||
|
||||
实现方法:
|
||||
## 实现
|
||||
|
||||
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
|
||||
- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
|
||||
### 1. 数据库的唯一索引
|
||||
|
||||
# 三、负载均衡的算法与实现
|
||||
当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
|
||||
|
||||
这种方式存在以下几个问题:
|
||||
|
||||
- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
|
||||
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
|
||||
- 不可重入,同一线程在没有释放锁之前无法再获得锁。
|
||||
|
||||
### 2. Redis 的 SETNX 指令
|
||||
|
||||
使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
|
||||
|
||||
SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
|
||||
|
||||
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
|
||||
|
||||
### 3. Redis 的 RedLock 算法
|
||||
|
||||
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
|
||||
|
||||
- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
|
||||
- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
|
||||
- 如果锁获取失败,会到每个实例上释放锁。
|
||||
|
||||
### 4. Zookeeper 的有序节点
|
||||
|
||||
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
|
||||
|
||||
**(一)抽象模型**
|
||||
|
||||
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
|
||||
|
||||
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
|
||||
|
||||
**(二)节点类型**
|
||||
|
||||
- 永久节点:不会因为会话结束或者超时而消失;
|
||||
- 临时节点:如果会话结束或者超时就会消失;
|
||||
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
|
||||
|
||||
**(三)监听器**
|
||||
|
||||
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
|
||||
|
||||
**(四)分布式锁实现**
|
||||
|
||||
- 创建一个锁目录 /lock;
|
||||
- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
|
||||
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
|
||||
- 执行业务代码,完成后,删除对应的子节点。
|
||||
|
||||
**(五)会话超时**
|
||||
|
||||
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。
|
||||
|
||||
**(六)羊群效应**
|
||||
|
||||
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
|
||||
|
||||
# 三、分布式 Session
|
||||
|
||||
在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。
|
||||
|
||||
<div align="center"> <img src="../pics//cookiedata.png"/> </div><br>
|
||||
|
||||
### 1. Sticky Sessions
|
||||
|
||||
需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
|
||||
|
||||
缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
|
||||
|
||||
### 2. Session Replication
|
||||
|
||||
在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
|
||||
|
||||
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
|
||||
|
||||
### 3. Persistent DataStore
|
||||
|
||||
将 Session 信息持久化到一个数据库中。
|
||||
|
||||
缺点:有可能需要去实现存取 Session 的代码。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
|
||||
|
||||
### 4. In-Memory DataStore
|
||||
|
||||
可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
|
||||
|
||||
# 四、负载均衡
|
||||
|
||||
## 算法
|
||||
|
||||
@ -174,116 +266,7 @@ PAC 服务器是用来判断一个请求是否要经过代理。
|
||||
|
||||
<div align="center"> <img src="../pics//52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg"/> </div><br>
|
||||
|
||||
# 四、分布式锁
|
||||
|
||||
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock,对于单机单进程应用,可以使用它们来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
|
||||
|
||||
## 使用场景
|
||||
|
||||
在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点上,比如订单服务分布在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如一个服务可以使用 APP 端或者 Web 端进行访问,如果一个用户同时使用 APP 端和 Web 端访问该服务,并且 APP 端的请求路由到了节点 A,WEB 端的请求被路由到了节点 B,这时候就需要使用分布式锁来进行同步。
|
||||
|
||||
## 实现方式
|
||||
|
||||
### 1. 数据库分布式锁
|
||||
|
||||
**(一)基于 MySQL 锁表**
|
||||
|
||||
该实现完全依靠数据库的唯一索引。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。
|
||||
|
||||
这种方式存在以下几个问题:
|
||||
|
||||
- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
|
||||
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
|
||||
- 不可重入,同一线程在没有释放锁之前无法再获得锁。
|
||||
|
||||
**(二)采用乐观锁增加版本号**
|
||||
|
||||
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
|
||||
|
||||
### 2. Redis 分布式锁
|
||||
|
||||
**(一)基于 SETNX、EXPIRE**
|
||||
|
||||
使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。
|
||||
|
||||
EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
|
||||
|
||||
**(二)RedLock 算法**
|
||||
|
||||
RedLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
|
||||
|
||||
- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
|
||||
- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
|
||||
- 如果锁获取失败,会到每个实例上释放锁。
|
||||
|
||||
### 3. Zookeeper 分布式锁
|
||||
|
||||
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
|
||||
|
||||
**(一)抽象模型**
|
||||
|
||||
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
|
||||
|
||||
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
|
||||
|
||||
**(二)节点类型**
|
||||
|
||||
- 永久节点:不会因为会话结束或者超时而消失;
|
||||
- 临时节点:如果会话结束或者超时就会消失;
|
||||
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
|
||||
|
||||
**(三)监听器**
|
||||
|
||||
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
|
||||
|
||||
**(四)分布式锁实现**
|
||||
|
||||
- 创建一个锁目录 /lock;
|
||||
- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
|
||||
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
|
||||
- 执行业务代码,完成后,删除对应的子节点。
|
||||
|
||||
**(五)会话超时**
|
||||
|
||||
如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。
|
||||
|
||||
**(六)羊群效应**
|
||||
|
||||
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
|
||||
|
||||
# 五、分布式 Session
|
||||
|
||||
在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。
|
||||
|
||||
<div align="center"> <img src="../pics//cookiedata.png"/> </div><br>
|
||||
|
||||
### 1. Sticky Sessions
|
||||
|
||||
需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
|
||||
|
||||
缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
|
||||
|
||||
### 2. Session Replication
|
||||
|
||||
在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
|
||||
|
||||
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
|
||||
|
||||
### 3. Persistent DataStore
|
||||
|
||||
将 Session 信息持久化到一个数据库中。
|
||||
|
||||
缺点:有可能需要去实现存取 Session 的代码。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
|
||||
|
||||
### 4. In-Memory DataStore
|
||||
|
||||
可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
|
||||
|
||||
# 参考资料
|
||||
|
||||
@ -298,3 +281,5 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
|
||||
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
|
||||
- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
|
||||
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
|
||||
- [微服务场景下的数据一致性解决方案](https://opentalk.upyun.com/310.html)
|
||||
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
|
||||
|
Reference in New Issue
Block a user