⭐CAP理论
CAP理论是说:在一个分布式系统中,最多只能满足C、A、P中的两个需求。
- C(Consistency):表示一致性,同一个数据的多个副本能够保持严格的一致性。
- A(Availability):表示可用性,系统提供的服务必须一直处于可用状态,每次请求都能得到正确的响应。
- P(Partation Tolerance):表示分区容错性,将同一个系统部署在多个服务器上,某一个系统宕机,其它系统可以提供同样的服务。
CAP之所以只能三选二,是因为在分布式系统中分区容错性是必须要实现的,不能因为某个节点故障导致整个系统不可用的情况,所以一致性和可用性只能二选一。
比如某个节点进行写操作,为了保证一致性,必须要禁止其它节点的读写操作,这就和可用性发生冲突了。
如果要保证其它节点也可以读写的话,一致性就没办法保证了。
对于保存系统元信息的节点,比如策略信息、数据库信息,必须保证所有节点的一致性,所以更适合CP架构。
对于读写频繁的节点,性能和可用性更加重要,所以更适合AP架构。
ZooKeeper:ZooKeeper保证的是CP,任何时刻对ZooKeeper的读请求都能得到一致性的结果,但是ZooKeeper在进行Leader选举的时候,或者半数以上机器不可用的时候,服务是不可用的。
Eureka:Eureka保证的是AP,在Eureka中不存在Leader节点,每个节点都是平等的,只要有一台节点是可用的,就可以提供正常的服务。只不过这台节点上的数据不一定是最新的。
Nacos:既支持CP也支持AP。
BASE理论
BASE理论可以理解为是CAP理论中AP架构的延伸,主要强调的是可用性。
基本可用:
当分布式系统出现故障时,允许损失部分功能的可用性,来保障核心功能的可用性。
比如通过消息队列缓存请求,避免大量请求直接进入后台,就是牺牲了响应时间的可用性,来保证核心功能的正常运行。
还有当系统无法支撑高并发访问时,对非核心业务降级(返回系统繁忙页面)。
最终一致性:
多个节点之间由于同步延迟的问题导致数据暂时不一致,经过一定时间之后,可以保证数据最终是一致的。
比如定时对账检测数据一致性,并修复。
⭐分布式锁
分布式锁是分布式环境下同一时刻只有一个节点可以访问共享资源。
分布式锁必须存放在所有节点都可用看到的地方,比如Redis、数据库等。
分布式锁的实现方式有:
- 基于关系型数据库实现分布式锁。
- 基于缓存实现分布式锁。
- 基于Zookeeper实现分布式锁。
- [fecing token方案](#fecing token方案)。
Zookeeper分布式锁的可靠性是最高的,而且有封装好的框架(Curator),实现起来也很简单。
基于关系型数据库实现分布式锁:
最简单的办法就是创建一张锁表。要锁住资源时,就在表中添加一条记录,释放锁时,就删除这条记录。
但是数据库是需要持久化到磁盘上的,频繁的读写数据库会导致IO开销很大,这种分布式锁适合对性能要求不高的场景。
⭐基于缓存实现分布式锁:
加锁时,用Redis的setnx key value
命令,这个命令的逻辑是这样的:当key不存在时,则设置key和value,并返回1,如果key已经存在了,直接返回0.
如果客户端加完锁之后就掉线了,那其它节点就再也获取不到锁了,所以还要通过set expire
命令设置过期时间。
需要注意的是,如果客户端执行完setnx
命令之后,还没来得及设置过期时间就掉线了,同样会产生死锁,所以需要Redis 2.6.12版本之后拓展了SET命令的参数:SET lock 1 EX 10 NX
。
缓存中的数据放在内存中,不需要额外的IO开销。也可以设置key的失效时间,来控制锁的超时时间,可以避免死锁问题。
⭐基于Zookeeper实现分布式锁:
- 加锁时,在zookeeper中/lock目录下创建一个临时有序节点,然后获取/lock目录下所有临时节点,并监听自己前面的那个节点(每个客户端只需要监听一个节点)。
- 然后再判断当前创建的节点是否是最小的节点。
- 如果是最小的节点,则认为加锁成功。
- 如果不是最小的节点,则对节点序号的前一个节点添加一个监听事件。
- 解锁时,删除当前最小的节点,注册了监听事件的客户端就会收到watcher事件,然后客户端再判断是否是最小的节点。
需要注意的是:
zookeeper需要频繁的添加和删除节点,所以性能不如缓存实现的分布式锁。
客户端与zookeeper建立连接后,zookeeper需要通过定时心跳来维持连接,如果zookeeper长时间没有收到客户端的心跳,就会认为客户端断开连接了,也会把对应的临时节点删除掉。如果出现网络波动,zookeeper有可能误判客户端下线,导致锁释放错误。
⭐fecing token方案:
fecing token方案流程如下:
- 客户端获取锁时,锁服务提供一个递增的token
- 客户端拿着这个token去操作共享资源
- 共享资源判断token是否是当前最新的token,如果是则执行操作,如果不是则拒绝执行。
如果要对数据库中的记录加锁,可以给每条记录新增一个token字段,记录更新成功对应的token+1。
要更新数据时先根据记录ID获取其最新的token,然后在update语句中将token作为更新条件:
1 | update t set value = ${value}, token = token+1 where id = ${id} and token = ${current_token} |
这个方案对共享资源的要求比较高,要有判断token和拒绝执行的能力,如果是写磁盘文件这样的场景就没法使用了。
⭐分布式事务
目前主流的思路有3种:
- 基于XA协议的两阶段提交;(强一致性)
- 三阶段提交;(强一致性)
- 基于消息的最终一致性;(最终一致性)
⭐基于XA协议的两阶段提交
两阶段提交的思路是:协调者下发事务操作,节点将操作结果通知协调者,协调者根据节点的反馈决定是提交还是回滚。
XA是一个分布式事务协议,规定了事务管理器和资源管理器接口。
事务管理器作为协调者,负责各个资源的提交和回滚;
资源管理器就是分布式事务的节点,通常是数据库,比如MySQL 5.5版本、Oracle、DB2都实现了XA接口。
两阶段提交分为投票和提交两个过程:
首先是投票阶段,协调者会向所有节点发起执行操作请求,并等待节点的响应。
节点收到请求后,会执行请求中的事务操作,但是不提交。执行完之后,根据执行结果会向协调者发送 “是” 或者 “否” 。
当所有节点都返回了操作结果后,就会进入提交阶段:
- 如果协调者接收的都是 “是” ,就会向所有节点发送提交请求,节点收到请求后,就会提交本地事务。
- 如果协调者收到的消息中包含 “否”,就向所有节点发送回滚请求,节点收到请求后,就会回滚之前的操作。
问题:
- 同步阻塞问题:在两阶段提交的过程中,当本地资源管理器占有资源时,其它资源管理器如果访问同一资源时会被阻塞。
- 单点故障问题:一旦事务管理器发生故障,资源管理器就会一直等待事务管理器的消息,导致一直锁定事务。
- 数据不一致问题:在提交阶段的时候,协调者发送提交请求后,如果发生了网络异常,就会导致可能只有一部分节点接收到了请求并提交了事务。其它节点无法执行事务,导致数据不一致。
⭐三阶段提交
三阶段提交是对两阶段提交的改进,在协调者和参与者引入了超时机制(2pc只是在协调者引入了超时)减少了整个集群的阻塞时间。
在提交阶段之前,新增一个预提交阶段,保证在最后提交阶段之前各节点的状态是一致的。
协调者向所有节点发送请求操作,参与者收到请求后,如果节点可以执行事务就回复 “是”,不然就回复 “否”。
协调者根据节点的回复,来决定是否进行预提交(PreCommit)
- 如果所有节点回复的都是Yes,协调者就会向所有节点发送预提交请求,节点收到预提交请求后,就会执行请求内的事务操作,并记录回滚信息,根据执行结果回复 “是”或”否”。
- 如果有任何一个节点回复了 “否”、或者协调者等待超时之后,就会向所有节点发送回滚事物的请求,节点收到回滚请求后,或者等待超时后,就会回滚事务。
当所有节点都返回了操作结果后,就会进入提交阶段:
- 如果协调者接收的都是 “是” 消息,就会向所有节点发送提交事务的请求,节点收到请求后,提交本地事务。然后给协调者发送响应。
- 如果协调者接收的都是 “否” 消息,就会向所有节点发送回滚事务的请求,节点收到请求后,就会回滚本地事务,然后给协调者发送响应。
问题:在提交阶段的时候,协调者发送提交请求后,如果发生了网络异常,就会导致可能只有一部分节点接收到了请求并提交了事务。其它节点无法执行事务,导致数据不一致。
⭐基于分布式消息的最终一致性
基于分布式消息的最终一致性方案,需要引入一个消息中间件,用于在多个系统之间传递消息。
假如在电商系统中的下单操作需要订单系统、支付系统、库存系统来完成。具体流程:
- 订单系统把订单消息发送给消息中间件,消息状态标记为 “待确认”。
- 消息中间件收到消息后,对消息持久化。并标记为 “待发送”。然后给订单系统返回持久化结果(成功/失败)
- 订单系统根据持久化结果判断如何进行业务操作。成功则创建订单,失败则返回订单创建失败。然后把订单创建结果返回给消息中间件。
- 消息中间件收到操作结果后,根据结果进行后续操作。成功则更新消息状态为 “可发送”,失败则删除消息。
- 消息中间件将 “可发送”的消息发送给支付系统,支付系统也按照上面的流程进行支付操作。
- 支付系统操作完成后,会将支付消息返回给消息中间件,中间件再把消息发送给订单系统。订单系统再调用库存系统,进行出库操作。
⭐补偿事务(TCC)
比如下单操作:
- Try阶段:扣库存。
- Confirm阶段:更新订单状态,如果更新失败,则进入Cancel阶段。
- Cancel阶段:恢复库存。
缺点:开发难度大,会导致每个业务操作都需要开发try、confirm、cancel三个接口,要保证数据一致性confirm和cancel阶段还需要实现幂等性。
分布式选举算法
Bully算法
Bully算法比较简单粗暴,它的选举规则是在所有活跃的节点中,选取ID最大的节点作为主节点。
在Bully算法中,节点分为普通节点和主节点,初始化时所有节点都是普通节点。选举完成后只有一个节点能成为主节点,当主节点故障后,才会重新选举。
Bully算法的选举过程,需要用到3种消息:
- Election消息:用于发起选举。
- Alive消息:对Election消息的应答。
- Victory消息:竞选成功后主节点向其它节点发送的消息。
集群中每个节点都需要维护其它所有节点的ID,具体的选举过程是:
- 每个节点判断自己的ID是否是当前所有活跃节点中最大的,如果是则向其它节点发送Victory消息。
- 如果不是最大的ID,则向比自己大的所有节点发送Election消息。
- 在一定时间内,某一节点没有收到其它节点回复的Alive消息,则认为自己是主节点,并向其它节点发送Victory消息;如果收到比自己ID大的节点的Alive消息,则等待其它节点发送Victory消息。
- 如果节点收到比自己ID小的节点发送的Election消息,则回复Alive消息。
Raft算法
Raft算法是少数服从多数的选举算法。
在Raft算法中,集群节点分为三种:
- Leader:主节点
- Candidate:后选择
- Follower:Leader的跟随者,不可以发起选举。
具体的选举过程是:
- 初始化时,所有节点都是Follower状态
- 开始选举时,所有节点变为Candidate,并向其它节点发送选举请求。
- 其它节点根据收到的选举请求的先后顺序,回复是否同意其成为主节点。每一轮选举中每个节点只能投一张票。
- 某一节点获得一半以上的票数则成为主节点,状态转为Leader,其它节点变为Follower。主节点和从节点会定期发送心跳包,检测主节点是否活跃。
- 当主节点故障,会重新发起选举。
ZAB算法
ZAB(Zookeeper Atomic Broadcast)算法是为Zookeeper实现分布式协调功能设计的。
ZAB算法通过节点ID和数据ID作为参考,进行选举。首先是数据ID最大的成为Leader,如果数据ID相同,则节点ID最大的成为Leader。
每个节点都有一个三元组(当前节点ID,当前节点数据ID,当前选举轮数)
ZAB算法中有三种角色:
- Leader:主节点
- Follower:跟随者
- Observer:观察者,无权投票。
选举过程中节点还会有4个状态:
- Looking状态:选举状态,Looking状态下的节点,会认为当前集群没有Leader,会自己进入选举状态。
- Leading状态:领导者状态,表示当前节点为Leader
- Following状态:跟随者状态,选出主节点后,其它节点更新为Following状态。
- Observing状态:观察者状态,表示当前节点为Observer,没有投票权和选举权。
具体的选举过程:
- 初始化时,所有节点会将元组内的信息,发送给其它节点。
- 每个节点都会用收到的选举信息和当前节点的选举信息进行比较。数据ID大的为Leader,数据ID一致的话,节点ID大的成为Leader。
- 选举完成后,Leader会向其它服务器发送心跳包并维护连接。