0%

分布式理论

⭐CAP理论

CAP理论是说:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

  • C(Consistency):表示一致性,同一个数据的多个副本能够保持严格的一致性。
  • A(Availability):表示可用性,系统提供的服务必须一直处于可用状态,每次请求都能得到正确的响应。
  • P(Partation Tolerance):表示分区容错性,将同一个系统部署在多个服务器上,某一个系统宕机,其它系统可以提供同样的服务。

CAP之所以只能三选二,是因为在分布式系统中分区容错性是必须要实现的,不能因为某个节点故障导致整个系统不可用的情况,所以一致性可用性只能二选一。

比如某个节点进行写操作,为了保证一致性,必须要禁止其它节点的读写操作,这就和可用性发生冲突了。

如果要保证其它节点也可以读写的话,一致性就没办法保证了。

对于保存系统元信息的节点,比如策略信息、数据库信息,必须保证所有节点的一致性,所以更适合CP架构。

对于读写频繁的节点,性能和可用性更加重要,所以更适合AP架构。

  • ZooKeeperZooKeeper保证的是CP,任何时刻对ZooKeeper的读请求都能得到一致性的结果,但是ZooKeeper在进行Leader选举的时候,或者半数以上机器不可用的时候,服务是不可用的。

  • EurekaEureka保证的是AP,在Eureka中不存在Leader节点,每个节点都是平等的,只要有一台节点是可用的,就可以提供正常的服务。只不过这台节点上的数据不一定是最新的。

  • Nacos:既支持CP也支持AP。

BASE理论

BASE理论可以理解为是CAP理论中AP架构的延伸,主要强调的是可用性。

BASE理论主要包括:基本可用最终一致性

基本可用

当分布式系统出现故障时,允许损失部分功能的可用性,来保障核心功能的可用性。

比如通过消息队列缓存请求,避免大量请求直接进入后台,就是牺牲了响应时间的可用性,来保证核心功能的正常运行。

还有当系统无法支撑高并发访问时,对非核心业务降级(返回系统繁忙页面)。

最终一致性

多个节点之间由于同步延迟的问题导致数据暂时不一致,经过一定时间之后,可以保证数据最终是一致的。

比如定时对账检测数据一致性,并修复。

分布式锁

分布式锁是分布式环境下同一时刻只有一个节点可以访问共享资源。

分布式锁必须存放在所有节点都可用看到的地方,比如Redis、数据库等。

分布式锁的实现方式有:

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实现分布式锁

  1. 加锁时,在zookeeper中/lock目录下创建一个临时有序节点,然后获取/lock目录下所有临时节点,并监听自己前面的那个节点(每个客户端只需要监听一个节点)。
  2. 然后再判断当前创建的节点是否是最小的节点。
    • 如果是最小的节点,则认为加锁成功。
    • 如果不是最小的节点,则对节点序号的前一个节点添加一个监听事件。
  3. 解锁时,删除当前最小的节点,注册了监听事件的客户端就会收到watcher事件,然后客户端再判断是否是最小的节点。

需要注意的是:

  1. zookeeper需要频繁的添加和删除节点,所以性能不如缓存实现的分布式锁。

  2. 客户端与zookeeper建立连接后,zookeeper需要通过定时心跳来维持连接,如果zookeeper长时间没有收到客户端的心跳,就会认为客户端断开连接了,也会把对应的临时节点删除掉。如果出现网络波动,zookeeper有可能误判客户端下线,导致锁释放错误。

fecing token方案

fecing token方案流程如下:

  1. 客户端获取锁时,锁服务提供一个递增的token
  2. 客户端拿着这个token去操作共享资源
  3. 共享资源判断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接口。

两阶段提交分为投票提交两个过程:

  1. 首先是投票阶段,协调者会向所有节点发起执行操作请求,并等待节点的响应。

  2. 节点收到请求后,会执行请求中的事务操作,但是不提交。执行完之后,根据执行结果会向协调者发送 “是” 或者 “否” 。

  3. 当所有节点都返回了操作结果后,就会进入提交阶段

    • 如果协调者接收的都是 “是” ,就会向所有节点发送提交请求,节点收到请求后,就会提交本地事务。
    • 如果协调者收到的消息中包含 “否”,就向所有节点发送回滚请求,节点收到请求后,就会回滚之前的操作。

问题

  • 同步阻塞问题:在两阶段提交的过程中,当本地资源管理器占有资源时,其它资源管理器如果访问同一资源时会被阻塞。
  • 单点故障问题:一旦事务管理器发生故障,资源管理器就会一直等待事务管理器的消息,导致一直锁定事务。
  • 数据不一致问题:在提交阶段的时候,协调者发送提交请求后,如果发生了网络异常,就会导致可能只有一部分节点接收到了请求并提交了事务。其它节点无法执行事务,导致数据不一致。

⭐三阶段提交

三阶段提交是对两阶段提交的改进,在协调者和参与者引入了超时机制(2pc只是在协调者引入了超时)减少了整个集群的阻塞时间。

在提交阶段之前,新增一个预提交阶段,保证在最后提交阶段之前各节点的状态是一致的。

  1. 协调者向所有节点发送请求操作,参与者收到请求后,如果节点可以执行事务就回复 “是”,不然就回复 “否”。

  2. 协调者根据节点的回复,来决定是否进行预提交(PreCommit)

    • 如果所有节点回复的都是Yes,协调者就会向所有节点发送预提交请求,节点收到预提交请求后,就会执行请求内的事务操作,并记录回滚信息,根据执行结果回复 “是”或”否”。
    • 如果有任何一个节点回复了 “否”、或者协调者等待超时之后,就会向所有节点发送回滚事物的请求,节点收到回滚请求后,或者等待超时后,就会回滚事务。
  3. 当所有节点都返回了操作结果后,就会进入提交阶段

    • 如果协调者接收的都是 “是” 消息,就会向所有节点发送提交事务的请求,节点收到请求后,提交本地事务。然后给协调者发送响应。
    • 如果协调者接收的都是 “否” 消息,就会向所有节点发送回滚事务的请求,节点收到请求后,就会回滚本地事务,然后给协调者发送响应。

问题:在提交阶段的时候,协调者发送提交请求后,如果发生了网络异常,就会导致可能只有一部分节点接收到了请求并提交了事务。其它节点无法执行事务,导致数据不一致。

⭐基于分布式消息的最终一致性

基于分布式消息的最终一致性方案,需要引入一个消息中间件,用于在多个系统之间传递消息。

假如在电商系统中的下单操作需要订单系统、支付系统、库存系统来完成。具体流程:

  1. 订单系统把订单消息发送给消息中间件,消息状态标记为 “待确认”。
  2. 消息中间件收到消息后,对消息持久化。并标记为 “待发送”。然后给订单系统返回持久化结果(成功/失败)
  3. 订单系统根据持久化结果判断如何进行业务操作。成功则创建订单,失败则返回订单创建失败。然后把订单创建结果返回给消息中间件。
  4. 消息中间件收到操作结果后,根据结果进行后续操作。成功则更新消息状态为 “可发送”,失败则删除消息。
  5. 消息中间件将 “可发送”的消息发送给支付系统,支付系统也按照上面的流程进行支付操作。
  6. 支付系统操作完成后,会将支付消息返回给消息中间件,中间件再把消息发送给订单系统。订单系统再调用库存系统,进行出库操作。

⭐补偿事务(TCC)

比如下单操作:

  • Try阶段:扣库存。
  • Confirm阶段:更新订单状态,如果更新失败,则进入Cancel阶段。
  • Cancel阶段:恢复库存。

缺点:开发难度大,会导致每个业务操作都需要开发try、confirm、cancel三个接口,要保证数据一致性confirm和cancel阶段还需要实现幂等性。

分布式选举算法

Bully算法

Bully算法比较简单粗暴,它的选举规则是在所有活跃的节点中,选取ID最大的节点作为主节点。

在Bully算法中,节点分为普通节点和主节点,初始化时所有节点都是普通节点。选举完成后只有一个节点能成为主节点,当主节点故障后,才会重新选举。

Bully算法的选举过程,需要用到3种消息:

  • Election消息:用于发起选举。
  • Alive消息:对Election消息的应答。
  • Victory消息:竞选成功后主节点向其它节点发送的消息。

集群中每个节点都需要维护其它所有节点的ID,具体的选举过程是:

  1. 每个节点判断自己的ID是否是当前所有活跃节点中最大的,如果是则向其它节点发送Victory消息。
  2. 如果不是最大的ID,则向比自己大的所有节点发送Election消息。
  3. 在一定时间内,某一节点没有收到其它节点回复的Alive消息,则认为自己是主节点,并向其它节点发送Victory消息;如果收到比自己ID大的节点的Alive消息,则等待其它节点发送Victory消息。
  4. 如果节点收到比自己ID小的节点发送的Election消息,则回复Alive消息。

Raft算法

Raft算法是少数服从多数的选举算法。

在Raft算法中,集群节点分为三种:

  • Leader:主节点
  • Candidate:后选择
  • Follower:Leader的跟随者,不可以发起选举。

具体的选举过程是:

  1. 初始化时,所有节点都是Follower状态
  2. 开始选举时,所有节点变为Candidate,并向其它节点发送选举请求。
  3. 其它节点根据收到的选举请求的先后顺序,回复是否同意其成为主节点。每一轮选举中每个节点只能投一张票。
  4. 某一节点获得一半以上的票数则成为主节点,状态转为Leader,其它节点变为Follower。主节点和从节点会定期发送心跳包,检测主节点是否活跃。
  5. 当主节点故障,会重新发起选举。

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,没有投票权和选举权。

具体的选举过程:

  1. 初始化时,所有节点会将元组内的信息,发送给其它节点。
  2. 每个节点都会用收到的选举信息和当前节点的选举信息进行比较。数据ID大的为Leader,数据ID一致的话,节点ID大的成为Leader。
  3. 选举完成后,Leader会向其它服务器发送心跳包并维护连接。