分布式理论

905 阅读36分钟

image.png

分布式系统的定义

硬件或软件分布在不同的网路计算机上,彼此间透过消息进行通信或协调的系统。

解决的问题(单体架构缺点)

  • 对海量用户处理能力有限。
  • 程序复杂性越高,开发效率越低。
  • 生产环境发生重大BUG,将导致整个服务瘫痪。
  • 代码量增加,编译效率下降。
  • 只能关注一套技术栈。

名词释义(分布式/集群/网络分区)

分布式:多个人在一起做不同的事。
集群:多个人在一起做相同的事。
网络分区(脑裂):网络之间不连通,导致分布式系统出现局部小集群,小集群间网络异常,小集群内部网路正常。

架构演进

单体应用架构->应用服务器和数据库服务器分离->应用服务器集群->数据库服务器主从复制,读写分离->增添搜索引擎缓解数据库压力->增加缓存缓解读库压力->数据库拆分(水平/垂直拆分)->应用服务器垂直拆分->应用服务器水平拆分。

一致性分类

1、强一致性:要求系统写入什么,读出来也是什么。性能影响大。
2、弱一致性:不承诺多久后数据能达到一致,尽可能在某个级别(秒/分/小时)达到一致状态。
3、读写一致性:第一时间看到自己更新的内容,其他人不保证。

  • 特定内容从主库读取,主库压力大。
  • 刚更新的内容从主库读取,过段时间后,从从库读取。
    4、最终一致性:仅保证最终系统内所有副本的数据是正确的。

image.png

CAP定理

1、Consistency(一致性):所有副本数据一致,从任意节点读取到的数据都是最新的。
2、Availablity(可用性):对外提供的服务保持正常,不会出现响应超时或响应错误。
3、Partition torerance(分区容忍性):当出现网络分区时,仍可对外提供服务。
任何分布式系统指哪个同时满足三个要求中的其中两个。例如:用户向N1发出请求,将值从vo改成v1,此时网络N1和N2之间发生中断,然后又有一个用户向N2发出请求获取该字段的值。此时有三种做法:

  • 将vo返回(牺牲一致性,AP模式)
  • 等待网络恢复,再返回v1(牺牲可用性,CP模式)
  • 将N1,N2合并(舍弃分布式技术,CA模式)

BASE理论

对CAP定理的权衡结果,如果无法做到强一致性,则根据业务特点,以适当方式实现最终一致性。
1、Basically Available(基本可用):当分布式系统出现故障时,允许损失部分可用性。(时间:正常是0.5秒响应结果,故障时可增加到1-2秒)。流量激增时,将部分用户引导到降级页面)
2、Soft State(软状态):允许数据存在中间状态(有部分数据尚未完成同步),但不影响系统整体可用性。
3、Eventuallly consistent(最终一致性):经过一段时间的同步后,数据最终能达到一致。

一致性协议(处理分布式事务)

2PC(两阶段提交)

流程

1、准备阶段:协调者给每个参与者发送prepare消息,运行本地事务但不提交。
2、提交阶段:协调者发现参与者运行失败或超时,则向参与者发送RollBack消息,否则发送Commit消息。

缺点

1、同步阻塞:在一阶段未到二阶段时,参与者事务都处于阻塞状态。
2、单点问题:如果协调者在运行二阶段时崩溃,参与者事务都处于锁定状态。
3、数据不一致:协调者在尚未发送完Commit消息就崩溃,将导致数据不一致。
4、过于保守:任意节点失败,将导致整个事务失败。

3PC(三阶段提交)

流程

1、CanCommit:协调者给每个参与者发送包含事务的请求,询问是否可以运行。
2、PreCommit:协调者要求参与者运行事务。
3、DoCommit:协调者要求参与者提交事务:此阶段参与者若无法收到协调者消息,超时默认提交事务。
降低了2PC的事务阻塞范围,但并未完全解决数据不一致的问题。

一致性算法(选出最终结果或Leader)

Paxos算法

角色

1、Client客户端:向分布式系统发出请求。
2、Proposer提案发起者:说服Acceptor达成一致。
3、Acceptor决策者:批准提案。
4、Leaner学习者:学习最终决策。

规范

1、一个Acceptor必须接受它收到的第一个提案。
2、每次收到的提案值,都必须跟第一次一样。
3、一个提案被选定,需要被半数以上的Acceptor接受。

流程1

1、Proposer向半数以上的Acceptor发起编号为N但没有Value的prepare请求。
2、如果Acceptor未接受过该提案,则返回null。
3、此时Proposer可以自行决定value值,向Accptor发起编号为N,值为value的accept请求。
4、Accpetor接受编号为N,值为Value的提案。

流程2

1、Proposer向半数以上的Acceptor发起编号为N+1但没有Value的prepare请求。
2、如果Acceptor已经接受过编号为N的提案,则返回提案N的Value值。
3、此时Proposer向Acceptor发起编号为N+1,值为Value的accept请求。
4、Acceptor接受编号为N+1,值为Value的提案。
极端情况:两个Proposer依次提交编号递增的提案,导致死循环。
解决:规定只有一个Proposer能提交提案。

Raft算法

角色

1、Leader领导者:与客户端交互,只有一个。
2、Candidate候选者:负责在选举过程中提名自己,当选举成功,成为领导者。
3、Follower跟随者:选民,等待投票通知。

流程

1、选举开始,所有节点都是Follower。
2、如果收到RequestVote(投票给我)、AppendEntries(已选出Leader)的请求,则保持Follower状态。
3、一段时间(随机150~300ms)内没收到请求,则将身份转换为Candidate开始竞选Leader,如果获得半数票数则成为Leader。
4、如果最后没选出Leader,则开启下一轮选举。

分布式系统设计策略

心跳检测

通常携带状态、元数据信息,方便管理。

  • 周期心跳检测:响应超时,判定死亡。
  • 累积失效检测:对濒临死亡的节点,发起有限次数重试。

高可用

  • 主备模式(常用):主机宕机,备机接管主机的一切工作。主机恢复后,通过自动热备或手动冷备的方式将服务切换回主机。
  • 互备模式(多主):两台主机同时运行,并且相互监测。
  • 集群模式:多个节点同时运行,透过主节点分担请求,需解决主节点高可用问题。

负载均衡

解决方案

  • 硬件:F5
  • 软件:LVS、HAProxy、Ngnix。
    策略:随机、轮询、权重、最少连接、哈希。

网路通信

RPC

Remote Procedure Call远程过程调用

角色

1、Client(客户端):服务调用方
2、Client Stub(客户端存根):将客户端请求打包成网络消息,通过网络发送给Server Stub。
3、Server Stub(服务端存根):接收Client Stub消息,将消息解包,调用本地方法。
4、Server(服务端):服务提供者。

image.png

RMI

Remote Method Invocation远程方法调用。Java原生支持,用于不同Java虚拟机之间的通信。

角色

客户端
1、Stub存根:客户端代理。
2、Remote Reference Layer:远程引用层,解析并运行远程引用协议。
3、Transport传输层:调用远程方法,接收运行结果。
服务端
1、Transport传输层:接收客户端请求,转发请求到Remote Reference Layer。
2、Remote Reference Layer:远程引用层,调用Skeleton方法。
3、Skeleton骨架:调用实际方法。
注册表Registry:以URL注册远程对象,并向客户端回复远程对象的引用。

image.png

image.png

分布式ID生成方案

传统的单体架构,基本都是单库业务单表的结构。每个业务表的ID一般都是从1开始自增,但是分布式架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据,这种情况使用数据库自增ID会产生相同ID的情况,不能保证主键的唯一性。

设计分布式ID需要考虑的几个方面

  • 全局唯一:不能出现重复ID,这是最基本的要求。
  • 趋势有序:业务上分页查询需求,排序需求,如果ID直接有序,则不必建立更多的索引,增加查询条件,而且Mysql InnoDB存储引擎主键使用聚簇索引,主键有序则写入性能更高。
  • 高可用:ID是一条数据的唯一标识,如果ID生成失效,则影响很大,业务执行不下去,所以好的ID生成方案需要高可用。
  • 信息安全:ID虽然趋势有序,但是不可以被看出规则,免得被爬取信息。

UUID

UUID,通用唯一标识码的缩写。UUID是由一组32位数的16进制数字组成,所以UUID理论上总数为16^32=2^128。生成的UUID是由8-4-4-4-12格式的数据组成,其中32个字符和4个连字符,一般我们会将连字符删除。目前UUID的产生方式有5种,每个版本的算法不同,应用范围也不同。
优点:生成方便,本地生成没有网络消耗。
缺点:不易于存储,信息不安全,对Mysql索引不利。

数据库生成

由于分布式数据库的起始自增值一样所以才会产生冲突,那么我们将分布式数据库的同一个业务表的自增ID设计成不一样的起始值,然后设置固定的步长,步长的值即为分库的数量或者分表的数量。
假设有三台机器,则DB1中order表的起始ID值为1,DB2中order表的起始值为2,DB3中order表的起始值为3,他们的自增的步长都是3,则他们的ID生成范围如下图所示:

image.png 优点:依赖数据库自身不需要其他资源,并且ID单调自增。
缺点:强依赖DB,当DB异常时整个系统不可用。虽然配置主从可以尽可能增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能导致重复发号。

使用Redis实现

Redis实现分布式唯一ID是通过INCR和INCRBY这样的自增原子命令,由于Redis本身的单线程特点,所以能保证生成的ID是唯一有序的。但是单机存在性能瓶颈,所以采用集群模式,集群模式跟数据库集群的问题一样,也需要设置分段和步长来实现。
Redis实现分布式ID的性能比较高,生成的数据也是有序的,但是依赖Redis,需要引进Redis组件,增加了系统的配置复杂性。

雪花算法-Snowflake

雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将64位分割成多个部分,每个部分代表不同含义。在Java中64bit的整数是long类型,所以在Java中雪花算法生成的ID就是long存储的。

  • 第1位占用1bit,值始终为0,可看做符号位不使用。
  • 第2位开始的41位是时间戳,41Bit可表示2^41个数,每个数表示毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L360024*365)=69 年的时间。
  • 中间的10bit可表示机器数,即2^10=1024个机器。
  • 最后12bit是自增序列,可表示2^12=4096个数。
    这样划分之后相当于在一毫秒一个数据中心的一台机器可产生4096个有序的不重复的ID。

image.png 雪花算法生成的ID是趋势递增的,不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也很高。但是强依赖机器时钟,如果机器时钟回拨,会导致发号重复或者服务处于不可用状态。

总结

分布式ID生成方案大致可以分为两类:

  • 一种是DB类型,根据设置不同起始值和步长来实现趋势递增,需要考虑服务的容错性和可用性。
  • 另一种是类snowflake类型,就是将64bit划分为不同的段,每段代表不同的含义,基本就是时间戳、机器ID和序列数。这种方案需要考虑时钟回拨的问题以及做一些buffer缓冲设计提高性能。可以将三者划分不同的位数来改变使用年限和并发数。

分布式锁

分布式锁是在分布式环境下锁定共享资源,让请求处理串行化,实际表现为互斥锁。分布式锁可以解决业务中的幂等性问题,如用户在下单支付,同时商户在改价,就可能出现并发问题。这个时候就需要串行化处理,防止出现业务问题。

分布式锁要解决的问题

  • 互斥性:同一时刻只能有一个进程(一台服务器)能访问资源。
  • 安全性:锁只能被持有该锁的客户端删除或释放。
  • 容错:在服务器宕机时,锁仍然可以得到释放或者其他服务器可以加锁。
  • 避免死锁。

分布式锁的设计目标

  • 强一致性
  • 服务高可用,系统稳健
  • 锁自动续约以及自动释放
  • 代码高度抽象与业务接入简单
  • 可监控管理

基于数据库实现分布式锁

  • 方案1:基于数据库表做乐观锁,用于分布式锁(version)
  • 方案2:基于数据库表做悲观锁(InnoDB的for update)
  • 方案3:基于数据库表数据记录做唯一约束(表中记录方法名称)

基于数据库表数据记录做唯一约束(表中记录方法名称)

要实现分布式锁最简单的方式是直接创建一张表,通过操作该表中的数据来实现。当我们要锁住某个方法或资源时,就插入一条记录,想要释放锁的时候就删除这条记录。

image.png 当我们想要锁住某个方法,执行以下sql

insert into methodLock(method_name, desc) values('method_name','desc');

因为我们对method_name做了唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁。
当方法执行完毕,我们要释放锁

delete from methodLock where method_name='method_name';

这种实现有以下几个问题:
1、这把锁强依赖数据库的可用性,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁失败,就会导致锁记录一直在数据库中,其他线程无法再获得锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程不会进入排队队列,想要再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得锁,因为锁记录已经存在了。

基于数据库表做悲观锁(InnoDB的for update)

在查询语句后增加for update,数据库会在查询过程中给数据库表加排他锁(这里要注意InnoDB引擎在加锁时,只有通过索引进行检索时才会使用行级锁,否则会使用表级锁,这里我们希望使用行级锁,就要给method_name添加索引,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题,重载方法的话建议把参数类型也加上)
当某条记录被加上排他锁后,其他线程无法再在该记录上增加排他锁,我们可以认为获得排他锁的线程即可获得分布式锁,当获得锁后,可以执行方法的业务逻辑,执行完方法之后,通过connnection.commit()操作来释放锁。这种方法可以有效解决上面无法释放锁和阻塞锁的问题,但还是无法解决数据库可用性和锁不可重入的问题。
这里还可能存在另一个问题,虽然我们对method_name使用了唯一索引,并且显式使用for update来使用行级锁,但是mysql会对查询进行优化,即使在条件中使用了索引字段,但是否使用索引来检索数据是由Mysql通过判断不同执行计划的代价来决定的,如果Mysql认为全表扫描比索引效率更高,那么就不会使用索引,这种情况下Mysql就会使用表锁,而不是行锁。

基于数据库表做乐观锁

乐观锁的含义:大多数基于数据版本记录实现。为数据增加一个版本标识,读取数据时,将版本号一并读出,之后更新时,对此版本号加一,在更新过程中,对版本号进行比较,如果一致才能更新成功。
缺点:增加了数据库的操作次数,原本一次的update变成select+update。如果是业务场景中的一次业务流程中,多个资源都需要保证数据一致性,那么如果全部使用基于数据库资源表的乐观锁,就要让每个资源都有一张资源表,这个在实际使用场景中肯定无法满足。

基于Redis实现分布式锁

使用redis的setNX()用于分布式锁(原子性)。SETNX是将key的值设为value,当且仅当key不存在时。若给定的key已经存在,则SETNX不做任何动作。

  • 返回1,表示进程获得锁,SETNX将键lock.id的值设置为锁的超时时间,当前时间+锁的有效时间。
  • 返回0,表示其他进程已经获得了锁,当前进程不能进入临界区,进程可以在一个循环中不断的尝试SETNX操作,直到获得锁成功。
    SETNX实现的分布式锁可能存在死锁的问题,与单机模式下的锁相比,分布式环境下不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。某个进程获得锁之后,断开了与Redis的连接,锁没有及时释放,竞争该锁的其他进程都会被阻塞,产生死锁的情况。解决方案是:需要对获取的锁进行超时时间设置,即setExpire,超时自动释放锁。

image.png

基于Zookeeper实现分布式锁

在Zookeeper实现分布式锁有两种方案,一是使用临时节点,二是使用临时有序节点。

临时节点

image.png 临时节点方案的原理:让多个进程竞争性的创建同一个临时节点,必然只有一个进程可以抢先创建成功。假设是进程1成功创建了节点,则它获得该分布式锁,此时其他进程需要在parent_node上注册监听,监听其下所有子节点的变化,并挂起当前进程。当parent_node下有子节点发生变化时,它会通知所有在其上注册了监听的进程。这些进程会判断是否是对应的锁节点上的删除时间。如果是则让挂起的进程尝试再次获得锁。
临时节点方案实现简单,但有缺点:

  • 缺点一:当parent_node下其他锁变动或者被删除时,进程2,3,4也会收到通知,但是他们并不关心其他锁的释放情况,这会带来网络开销。
  • 缺点二:采用临时节点方案创建的锁是非公平的。

临时有序节点

image.png

  • 每个进程都会尝试在parent_node下创建临时有序节点。
  • 然后每个进程需要获取当前parent_node下所有临时节点信息,并判断自己是否是最小的一个节点,如果是则获取该锁。如果不是则挂起当前进程,并对前一个节点注册监听。
  • 当进程释放锁,会删除对应的节点,前一个节点删除后,会通知下一个节点,下一个节点的进程则可以尝试获得锁。 每个临时有序节点只需要关心自己的上一个节点,而不需要关心其他的额外节点和额外事件,而且实现的锁是公平的(有顺序的,先到达的进程先获得锁)。 临时有序节点的另一个优点是可以实现共享锁,比如读写锁中的读锁,如下图所示,可以将临时有序节点分为读锁节点和写锁节点:
  • 对于读锁节点,只需要关心前一个写锁节点的释放,如果前一个写锁释放了,则多个读锁节点对应的线程可以并发的读取数据。
  • 对于写锁节点,只需要关心前一个节点的释放,不管是读锁节点还是写锁节点。

分布式事务解决方案

什么是事务

事务提供一种机制将一个活动涉及的所有操作纳入一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单的说事务就是“要么什么都不做,要么做全套”的机制。

数据库事务ACID属性

  • 原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间的某个环节。
  • 一致性:在事务开始之前和结束之后,数据库数据的一致性约束没有被破坏。
  • 隔离性:数据库允许多个事务并发执行,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个未提交,它所访问的数据就不受未提交事务的影响。
  • 持久性:事务执行结束后,对数据的修改是永久的,即使系统故障也不会丢失。
    总之,原子性描述的是事务的整体性,一致性和隔离性描述的是事务的数据正确性,持久性描述的是事务对数据修改的可靠性。

什么是分布式事务

概念

随着互联网快速发展,SOA、微服务等架构被使用,现在分布式系统一般由多个独立的子系统组成,多个子系统通过网络通信互相协作配合完成各个功能。例如电子商务中的下单支付流程,至少会涉及到交易系统和支付系统,而且这个过程会涉及事务概念,即保证交易系统和支付系统的数据一致性,此时我们称这种跨系统的事务为分布式事务。简单说,分布式事务就是事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的机器节点上。

难点

  • 事务的原子性:事务操作跨不同节点,当多个节点中某一节点操作失败时,需要保证多节点操作的“要么都不做,要么做全套”的原子性。
  • 事务的一致性:当发生网络传输故障或节点故障时,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。
  • 事务的隔离性:在分布式事务控制中,可能出现提交不同步的现象,这个时候有可能出现“部分已经提交”的事务,此时并发应用访问数据如果没有控制,有可能出现“脏读”问题。

分布式系统的一致性

CAP理论

image.png CAP理论又被称作布鲁尔定理,是指一个分布式系统(相互连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性、可用性和分区容错性三者中的两个,另外一个必须被牺牲。

  • 一致性:对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。一致性强调客户端读操作能够获取最新的写操作结果,是因为事务在执行过程中,客户端无法读取到未提交的数据,只有等到事务提交后,客户端才能读取到事务写入的数据,而如果事务失败则会进行回滚,客户端也不会读取到事务中间写入的数据。
  • 可用性:非故障性节点在合理的时间内返回合理的响应(不是错误和超时的响应)。这里强调的是合理的响应,不能超时和出错。
  • 分区容错性:当出现网络分区后,系统能够继续履行职责。

虽然CAP理论只能选择其中两个,放在分布式环境下,我们必定会选择P(分区容忍),因为网络不是100%可靠的。例如我们选择CA放弃了P,那么当发生分区时,为了保证C,系统需要禁止写入,当有写入请求系统返回error,这又和A冲突了,因为A要求返回no error和no timeout。 因此,分布式系统理论上只能选择CP(一致性+分区容忍性)或者AP(可用性+分区容忍性),在一致性C和可用性A之间做这种选择。

BASE理论

image.png BASE理论是指基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventual Consistency)。

  • BA:基本可用,分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • S:软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性,这里的中间状态就是CAP理论中的数据不一致。
  • E:最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
    CAP理论是忽略延时的,而实际应用中延时是无法避免的,这一点意味着完美的CP场景是不存在,因此CAP中的CP方案实际上也是实现了最终一致性,只是”一定时间“是几毫秒而已。
    AP方案中牺牲一致性只是发生在分区发生故障时,而不是永远放弃一致性,这一点就是BASE理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性。

数据一致性

  • 强一致性:要求无论更新操作是在哪个数据副本上执行,之后所有的读操作都要能获得最新的数据。
  • 弱一致性:在这种一致性协议下,用户读到某一操作对系统特定数据的更新需要一段时间,我们将这段时间称为”不一致性窗口“。
  • 最终一致性:是弱一致性的一种特例,在这种一致性下系统保证用户最终能读取到某操作对系统特定数据的更新(读取操作之前没有改数据的其他更新操作)。”不一致性窗口”的大小依赖于交互延迟、系统的加载以及数据的副本数等。

柔性事务

基于BASE理论的设计思想,柔性事务下,在不影响系统整体可用性的情况下,允许系统存在数据不一致的中间状态(Soft State软状态),在经过数据同步的延时之后,最终达到数据一致,并不是完全放弃了ACID,而是通过放宽一致性要求,借助本地事务来实现分布式事务一致性的同时也保证系统的吞吐。

XA和2PC,3PC

XA的全称是eXtended Architecture,是1991年由X/Open发布的规范,用于分布式事务处理(DTP),它是一个分布式事务协议,它通过二阶段提交协议保证强一致性,DTP模型已经成为事务模型组件行为的事实上的标准。
下图是Open Group标准文件中对DTP模型部分截图

image.png DTP模型抽象AP(应用程序)、TM(事务管理器)和RM(资源管理器)的概念来保证分布式事务的强一致性。其中RM和TM之间采用XA的协议进行双向通信。
与传统的本地事务相比,XA事务增加了准备阶段,数据库除了被动接受提交指令外,还可以反向通知调用方事务是否可以被提交。TM可以手机所有分支事务的准备结果,并在最后进行原子提交,来保证事务的强一致性。
Java通过定义JTA接口实现XA模型,JTA接口中的ResourceManager需要数据库厂商提供XA驱动实现,Transaction Manager需要事务管理器的厂商实现,传统的事务管理器需要同应用服务器绑定,因此成本很高,而嵌入式的事务管理器可以以jar包的形式提供服务。

2PC(两阶段提交)

二阶段的思路就是:参与者将操作成败告知协调者,再由协调者根据参与者的反馈决定各参与者是否提交还是终止操作。
所谓两个阶段是指:

  • 1、准备阶段(投票阶段)
  • 2、提交阶段(执行阶段) 两阶段协议有优点,比较简单,但缺点也很明显:
  • 1、同步阻塞:所有事务参与者在等待其他事务参与者响应时都处于同步阻塞状态,无法进行其他操作。
  • 2、单点问题:协调者在2PC中起到很大作用,发生故障将会造成很大影响,特别是在阶段二发生故障,所有参与者会一直等待。
  • 3、数据不一致:在阶段二,如果协调者只发送了部分Commit消息,此时网络发生异常,那么只有部分参与者提交了事务,导致数据不一致。
  • 4、太过保守:任一节点失败都会导致整个事务失败,没有完善的容错机制。

3PC(三阶段提交)

三阶段提交是为解决两阶段的缺点设计的,三阶段是“非阻塞”协议。与两阶段相比,三阶段有两个改动点:

  • 引入超时机制,同时在协调者和参与者中都引入了超时机制。
  • 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
    也就是说,3PC把2PC的准备阶段再次一分为二,三阶段提交变成CanCommit、PreCommit和DoCommit三个阶段。

image.png 3PC相对于2PC的优点是:解决了单点故障,减少阻塞。
3PC的问题:数据一致性问题。因为由于网络原因,协调者发送的abort响应无法及时被参与者接收到,那么参与者在等待超时后执行了commit操作,这样就和其他接受到abort响应并回滚的参与者存在数据不一致的情况。

分布式事务解决方案

TCC

TCC(Try-Confirm-Cancel)事务机制有三个阶段:

  • Try阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性)。
  • Confirm阶段:确认真正执行业务,不做任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性,要求具备幂等设计,Confirm失败后需要进行重试。
  • Cancel阶段:取消执行,释放Try阶段预留的业务资源。Cancel操作满足幂等性。
    TCC事务机制相比于XA,解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
    基于TCC实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为Try、Confirm和Cancel三个接口,所以代码实现复杂度相对较高。

本地消息表

本地消息表方案中有消息生产者和消费者两个角色,假设系统A是消息生产者,系统B是消息消费者,大致流程如下:

image.png

  • 1、当系统A被其他系统调用发生数据更新操作,首先会更新数据库的业务表,其实是往相同数据库的消息表插入一条数据,两个操作发生在一个事务中。
  • 2、系统A的脚本定期轮询本地消息往mq中写入一条消息,如果消息发送失败会进行重试。
  • 3、系统B消息mq中的消息,并处理业务逻辑。如果本地事务处理失败,会继续消费mq中的消息进行重试。如果业务上的失败,可以通知系统A进行回滚操作。

本地消息表实现的条件:

  • 消费者和生产者的接口都支持幂等。
  • 生产者需要额外的创建消息表。
  • 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作。

容错机制:

  • 步骤1失败,事务直接回滚。
  • 步骤2、3写mq与消费mq失败会进行重试。
  • 步骤3业务失败,系统B向系统A发起事务回滚操作。
    此方案的核心是将需要分布式处理的任务以消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。有点类似于mysql主从复制。

可靠消息最终一致性

大致流程如下

image.png

1、A系统先向mq发送一条prepare消息(半消息),如果prepare消息发送失败,则直接取消操作。
2、如果消息发送成功,则执行本地事务。
3、如果本地事务执行成功,则向mq发送一条confirm消息,如果发送失败,则回滚本地事务。
4、B系统定期消费mq中的confirm消息,执行本地事务,并发送ack消息。如果B事务的本地事务失败,会一直不断重试,如果是业务失败,会向A系统发起回滚请求。
5、mq会定期轮询所有prepare消息调用系统A提供的接口查询A的本地事务处理情况,如果该prepare消息对应的本地事务处理成功,则重新发送confirm消息,否则直接回滚该消息。

尽最大努力通知

尽最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。这个方案的大致意思是:

  • 系统A本地事务执行完之后,发送这个消息到MQ。
  • 这里会有个消费MQ的服务,这个服务会消费MQ的消息并调用系统B的接口。
  • 要是系统B执行成功就ok了,要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N此次,最后还是不行就放弃。

image.png

一致性hash算法

Hash算法

Hash,一般翻译成散列、杂凑或者译为哈希,是把任意长度的输入(又叫做预映射)通过散列算法变换为固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是散列值的空间通常远小于输入的空间,不同的输入可能散列成相同的输出。简单说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
hash就是把输入值“压缩”并转成更小的值,这个值通常情况下是唯一、格式紧凑的。现在的业务系统多是微服务架构,在天然的分布式特性下,hash算法就不太合适了。比如在分布式系统中,要将数据存储到具体的节点上,如果我们采用普通的hash算法进行路由,将数据映射到具体的节点上,如key%N,key就是数据的key,N是机器节点数,有一个机器加入或者退出集群都会导致数据映射失效,这会造成大量数据重新hash,影响业务的正常运行,这时候就需要一致性hash算法了。

一致性Hash算法

一致性Hash算法是一种特殊的哈希算法,目的是解决分布式缓存问题。在移除或者添加一个服务器时,能够尽可能小的改变已存在的服务器请求与处理请求服务器之间的映射关系。一致性hash算法解决了简单hash算法在分布式哈希表中存在的动态伸缩问题。

均衡性

均衡性是指哈希的结果能够尽可能分布到所有的缓冲节点中去,这样可以使得所有的缓冲空间得到利用。

单调性

单调性是指当缓冲区变化时一致性哈希能够尽量保护已分配的内容不会重新映射到新的缓冲区,来减少大量的重新hash,提高了性能。

分散性

在分布式环境中,终端有可能看不到所有缓存,只能看到其中一部分。当终端希望通过哈希过程将内容映射到缓存上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是将相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓存中去,降低了系统存储的效率。

负载

负载问题实际上是从另一个角度看到分散性问题。既然不同的终端可能将相同的内容映射到不同的缓存实例中,那么对于一个特定的缓存实例而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应该避免的,因此好的hash算法应该尽量降低缓冲的负载。

什么是一致性哈希

  • 1、首先求出redis服务器节点的哈希值,并将其配置到0-2^32的圆上。
  • 2、然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
  • 3、然后从数据映射的位置开始顺时针查找,将数据保存到找到的第一个服务器,如果超过2^32仍然找不到服务器,就会保存到第一台redis服务器上。

image.png

从上图的状态中添加一台redis服务器,采用余数分布式算法,会由于保存键的缓存实例发生变化而影响缓存的命中。但一致性哈希算法中,只有在增加节点(node5)的逆时针的一小部分hash会受到影响。

image.png 这种方式很好解决了缓存命中率、容错性和可扩展性,但是当服务节点很少的时候,就会带来另一个问题,就是“数据倾斜”,也就是很多key被分配到同一个服务器节点上,这种情况隐患很大,如果这个节点突然挂掉,就会造成缓存雪崩。那么如何解决呢?--虚拟节点。
假如我们只有两台服务器,分布如下

image.png 此时必然造成大量数据集中到Redis2上。为了解决数据倾斜问题,引入虚拟节点,对每个服务节点计算多个哈希,每个计算结果都放置一个服务节点,称为“虚拟节点”。具体做法可以在服务器ip或主机名后面增加编号来实现,例如我们为每台服务器计算2个虚拟节点,于是计算出Redis1#1,Redis1#2,Redis2#1,Redis2#2,就形成4个虚拟节点

image.png

image.png