分布式基础知识点
1. 什么是分布式?
(1)含义
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是 利用更多的机器,处理更多的数据 。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
(2)挑战
分布式系统需要大量机器协作,面临诸多的挑战:
- 异构的机器与网络:分布式系统中的机器,配置不一样,其上运行的服务也可能由不同的语言、架构实现,因此处理能力也不一样;节点间通过网络连接,而不同网络运营商提供的网络的带宽、延时、丢包率又不一样。怎么保证大家齐头并进,共同完成目标,这四个不小的挑战。
- 普遍的节点故障:虽然单个节点的故障概率较低,但节点数目达到一定规模,出故障的概率就变高了。分布式系统需要保证故障发生的时候,系统仍然是可用的,这就需要监控节点的状态,在节点故障的情况下将该节点负责的计算、存储任务转移到其他节点
- 不可靠的网络:节点间通过网络通信,而网络是不可靠的。可能的网络问题包括:网络分割、延时、丢包、乱序。相比单机过程调用,网络通信最让人头疼的是超时:节点A向节点B发出请求,在约定的时间内没有收到节点B的响应,那么B是否处理了请求,这个是不确定的,这个不确定会带来诸多问题,最简单的,是否要重试请求,节点B会不会多次处理同一个请求。
总而言之,分布式的挑战来自 不确定性 ,不确定计算机什么时候crash、断电,不确定磁盘什么时候损坏,不确定每次网络通信要延迟多久,也不确定通信对端是否处理了发送的消息。而分布式的规模放大了这个不确定性,不确定性是令人讨厌的,所以有诸多的分布式理论、协议来保证在这种不确定性的情况下,系统还能继续正常工作。
而且,很多在实际系统中出现的问题,来源于设计时的盲目乐观,觉得这个、那个应该不会出问题。
0、基本原则
CAP指的是在一个分布式系统中:
- 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
- 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择)
这三个要素最多只能同时实现两点,不可能三者兼顾。
如果你的实际业务场景,更需要的是保证数据一致性。那么请使用CP类型的分布式锁,比如:zookeeper,它是基于磁盘的,性能可能没那么好,但数据一般不会丢。
如果你的实际业务场景,更需要的是保证数据高可用性。那么请使用AP类型的分布式锁,比如:redis,它是基于内存的,性能比较好,但有丢失数据的风险。
其实,在我们绝大多数分布式业务场景中,使用redis分布式锁就够了,真的别太较真。因为数据不一致问题,可以通过最终一致性方案解决。但如果系统不可用了,对用户来说是暴击一万点伤害。
一般来说,用Redis控制共享资源并且还要求数据安全要求较高的话,最终的保底方案是对业务数据做幂等控制,这样一来,即使出现多个客户端获得锁的情况也不会影响数据的一致性。
#1、分布式服务接口幂等性
其实保证幂等性(比如不能重复扣款)主要是三点:
(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单ID,一个订单ID最多支付一次,对吧
(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见得方案是再mysql中记录个状态啥得,比如支付之前记录一条这个订单得支付流水,而且支付流水采用order id作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际得支付扣款
(3)每次接收请求需要进行判断之前是否处理过得逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,order id已经存在了,唯一键约束生效,报错插入不进去得。然后你就不用再扣款了。
#2、zk的使用场景
(1)分布式协调:这个其实就是zk很经典的一个用法,简单来说,就好比,你系统A发送个请求到mq,然后B消费了之后处理。那A系统如何指导B系统的处理结果?用zk就可以实现分布式系统之间的协调工作。A系统发送请求之后可以在zk上对某个节点的值注册个监听器,一旦B系统处理完了就修改zk那个节点的值,A立马就可以收到通知,完美解决。
(2)分布所锁:对某一个数据联系发出两个修改操作,两台机器同时收到请求,但是只能一台机器先执行另外一个机器再执行,那么此时就可以使用zk分布式锁,一个机器接收到了请求之后先获取zk上的一把分布式锁,就是可以去创建一个znode,接着执行操作,然后另外一个机器也尝试去创建那个znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等等一个机器执行完了自己再执行。
zk分布式锁,其实做的比较简单,就是某个节点尝试创建临时znode(防止死锁呗),此时创建成功了就获取了这个锁,这个时候别的客户端来创建锁会失败,只能注册监听器来监听这个锁,释放锁就是删除这个znode,一旦释放掉就会反向通知客户端,然后等待着的客户端就可以再次尝试重新加锁。
(3)配置信息管理:zk可以用作很多系统的配置信息的管理,比如kafka,storm等等很多分布式系统都会选用zk来做一些元数据,配置信息的管理
(4)HA高可用性:这个应该是很常见的,比如hdfs,yarn等很多大数据系统,都选择基于zk来开发HA高可用机制,就是一个重要进程一般会主备两个,主进程挂了立马通过zk感知到切换到备份进程
3、分布式锁
##(1)分布式锁特性
- 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
- 高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
- 防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
- 独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了;
##(2)分布式锁的缺陷
- 客户端长时间阻塞导致锁失效问题:客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
- redis服务器时钟漂移问题:如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
- 单点实例安全问题:如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁。
(2)Redis中lua脚本保证操作原子性
redis中使用lua脚本,可以保证操作的原子性,原因:
1 | “Atomicity of scripts |
()Redis分布式锁
说道Redis分布式锁大部分人都会想到:setnx+lua
,或者知道 set key value px milliseconds nx
。后一种方式的核心实现命令如下:
1 | - 获取锁(unique_value可以是UUID等) |
这种实现方式有3大要点(也是面试概率非常高的地方):
- set命令要用
set key value px milliseconds nx
; - value要具有唯一性;
- 释放锁时要验证value值,不能误解锁;
事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
- 在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 导致锁丢失。
##()Redlock算法
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
##()Redis可重入锁
加锁主要是通过以下脚本实现的:
1 | if (redis.call('exists', KEYS[1]) == 0) |
其中:
- KEYS[1]:锁名
- ARGV[1]:过期时间
- ARGV[2]:uuid + “:” + threadId,可认为是requestId
- 先判断如果锁名不存在,则加锁。
- 接下来,判断如果锁名和requestId值都存在,则使用hincrby命令给该锁名和requestId值计数,每次都加1。注意一下,这里就是重入锁的关键,锁重入一次值就加1。
- 如果锁名存在,但值不是requestId,则返回过期时间。
释放锁主要是通过以下脚本实现的:
1 | if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) |
- 先判断如果锁名和requestId值不存在,则直接返回。
- 如果锁名和requestId值存在,则重入锁减1。
- 如果减1后,重入锁的value值还大于0,说明还有引用,则重试设置过期时间。
- 如果减1后,重入锁的value值还等于0,则可以删除锁,然后发消息通知等待线程抢锁。
()读写锁与锁分段
提升redis分布式锁性能
- 区分读写锁
- 将大锁分段:在java中ConcurrentHashMap,就是将数据分为16段,每一段都有单独的锁,并且处于不同锁段的数据互不干扰,以此来提升锁的性能
()自动续期
自动续期的功能是获取锁之后开启一个定时任务,每隔10秒判断一下锁是否存在,如果存在,则刷新过期时间。如果续期3次,也就是30秒之后,业务方法还是没有执行完,就不再续期了。
在实现自动续期功能时,还需要设置一个总的过期时间,可以跟redisson保持一致,设置成30秒。如果业务代码到了这个总的过期时间,还没有执行完,就不再自动续期了。
##()Redis分布式锁与ZK比较
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能
zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较低
另外一点就是,如果redis获取锁的那个客户端bug了,或者挂了,那么等待超时时间之后才能释放锁,而zk的话,因为创建的是临时节点,只要客户端挂了,znode就没了,此时就会自动释放锁
()乐观锁和悲观锁
乐观锁可以使用CAS和版本号机制来实施
(1)CAS(Compare And Swap):CAS操作包括了3个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 拟写入的新值(B)
(2)版本号机制
除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
乐观锁和悲观锁比较:
1、功能限制
与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
2、竞争激烈程度
如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
乐观锁加锁吗?
(1)乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。
(2)有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。
4、分布式事务
5、分布式一致性算法
Raft
参考文档
(1)什么是分布式系统,如何学习分布式系统:https://www.cnblogs.com/xybaby/p/7787034.html