一· 分布式锁
在单机场景下,可以使用语言的内置锁
来实现进程同步
。但是在分布式场景下,需要同步的进程
可能位于不同的节点
上,那么就需要使用分布式锁
。
阻塞锁通常使用互斥量来实现:
- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
- 互斥量为 1 表示未锁定状态。
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。
1.1 数据库的唯一索引
获得锁
时向表中插入一条记录
,释放锁
时删除这条记录
。唯一索引
可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否处于锁定状态。
存在以下几个问题:
- 锁没有
失效时间
,解锁失败
的话其它进程无法再获得该锁; - 只能是非阻塞锁,插入失败直接就报错了,无法重试;
- 不可重入,已经获得锁的进程也必须重新获取锁。
1.2 Redis 的 SETNX 指令
使用 SETNX
(set if not exist)指令插入一个键值对,如果 Key 已经存在
,那么会返回 False
,否则插入成功并返回 True。
SETNX 指令
和数据库的唯一索引类似
,保证了只存在
一个 Key 的键值对
,那么可以用一个 Key 的键值对
, 是否存在
来判断是否存于锁定状态
。
EXPIRE 指令
可以为一个键值对设置一个过期时间
,从而避免了
数据库唯一索引实现方式中释放锁失败
的问题。
1.3 竞态模型场景
为了避免Redis宕机
引起锁服务不可用
, 需要为Redis实例(master)增加热备(slave),如果master不可用则将slave提升为master。这种主从的配置方式
存在一定的安全风险
,由于Redis的主从复制是异步进行的, 可能会发生多个客户端
同时持有一个锁
的现象。
竞态模型:
Client A
获得在master节点获得了锁- 在master将key备份到slave节点之前,
master宕机
slave
被提升
为master
Client B
在新的master节点处获得了锁,Client A
也持有这个锁。
解决方法:分布式下的RedLock算法
1.4 Redis 的 RedLock 算法
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
- 尝试从 N 个
互相独立
Redis 实例获取锁
; - 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功;
- 如果获取锁失败,就到
每个实例上释放锁
。
1.5 Zookeeper 的有序节点
- Zookeeper 抽象模型 Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节点为 /app1。
-
节点类型
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。
- 监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
-
分布式锁实现
- 创建一个锁目录 /lock;
- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
- 执行业务代码,完成后,删除对应的子节点。
-
会话超时
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,这种实现方式不会出现数据库的唯一索引实现方式释放锁失败的问题。
- 羊群效应
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应,一只羊动起来,其它羊也会一哄而上),而我们只希望它的后一个子节点收到通知。
二 分布式事务
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
分布式锁
和分布式事务
区别:
- 锁问题的关键在于进程操作的
互斥关系
,例如多个进程
同时修改账户的余额
,如果没有互斥关系
则会导致该账户的余额不正确。 - 而事务问题的关键则在于事务
涉及的一系列操作
需要满足ACID
特性,例如要满足原子性操作
则需要这些操作要么都执行
,要么都不执行
。
1 2PC
两阶段提交
(Two-phase Commit,2PC),通过引入协调者
(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
1.1 运行过程
1.准备阶段 协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。
2.提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
1.2 存在问题
- 同步阻塞
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞等待状态,无法进行其它操作。
- 单点问题
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在提交阶段发生故障,所有参与者会一直同步阻塞等待,无法完成其它操作。
- 数据不一致
在提交阶段,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
- 太过保守
任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
2 本地消息表
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
-
在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
-
之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
-
在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
三 CAP
分布式系统不可能同时满足一致性
(C:Consistency)、可用性
(A:Availability)和分区容忍性
(P:Partition Tolerance),最多只能同时满足其中两项。
1. 一致性
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
对系统的一个数据更新成功
之后,如果所有用户都能够读取到最新的值
,该系统就被认为具有强一致性。
2. 可用性
可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
3. 分区容忍性
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
4. 权衡
在分布式系统中,分区容忍性必不可少
,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
-
为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
-
为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
四 BASE
BASE 是基本可用
(Basically Available)、软状态
(Soft State)和最终一致性
(Eventually Consistent)三个短语的缩写。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
1. 基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
2. 软状态
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
3. 最终一致性
最终一致性
强调的是系统中所有的数据副本,在经过一段时间
的同步后,最终能达到一致的状态。
ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。