分布式
CAP理论
CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。
在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:
- 一致性(Consistency) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
什么是网络分区?
分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。
提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。
然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。
简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。
为啥不可能选择 CA 架构呢? 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。
另外,需要补充说明的一点是: 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。
BASE理论
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
BASE 理论的核心思想
即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。
1. 基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
什么叫允许损失部分可用性呢?
- 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
- 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
2. 软状态
软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
3. 最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式一致性的 3 种级别:
- 强一致性 :系统写入了什么,读出来的就是什么。
- 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
- 最终一致性 :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。
业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
那实现最终一致性的具体方式是什么呢? 《分布式协议与算法实战》open in new window 中是这样介绍:
- 读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
- 写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。
- 异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
比较推荐 写时修复,这种方式对性能消耗比较低。
分布式 id
当有多台数据库服务器,要进行写操作时,由于服务器之间是独立的,每台服务器的id按理说是可以重复的,但是这样会在同步时产生问题,所以要考虑分布式id,主要有以下几种方式:
UUID 生成唯一ID,由客户端携带ID写入数据库中
但一般不会使用UUID,没有递增特性,插入到innoDB,查找时会对数据库造成巨大压力,不能作为物理主键,只能作为逻辑主键,物理主键依然使用自增id。( 物理主键,可以在系统中由数据库自动生成;而逻辑主键一般是用来表示一个包含确切意义的并唯一的键值,可根据逻辑主键的值了解到一些具体信息。)
数据库自增ID 增加一个数据库表用来生成id,每次写入数据前先插入生成id的数据库一个数据,获取一个递增id(要访问两次数据库)
Redis自增ID 首先访问Redis,使用incr()生成一个递增id
雪花算法 生成唯一ID,而且自增
数据库号段模式 在数据库中首先取一段数据放到内存里,用到的时候直接从内存中拿
分布式锁
基于数据库 创建一张锁表,想要锁住某个方法或者资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录,根据主键的唯一性,如果其他请求来申请锁,也要插入同一条数据,插入不进去就代表有请求占有锁
基于Redis
分布锁一般通过
redis
实现,主要通过setnx
函数向redis
保存一个key,value
等于保存时的时间戳,并设置过期时间,然后返回true
;当获得锁超过等待时间返回
false
;通过
key
获取redis
保存的时间戳,如果value
不为空,并且当前时间戳减去value
值超过锁过期时间返回false
如果一次没有获得锁,则每隔一定时间(
10ms
或者20ms
)再获取一次,直到超过等待时间返回false
。基于Zookeeper
分布式事务(分布式事务( 图解 + 秒懂 + 史上最全 ) - 疯狂创客圈 - 博客园 (cnblogs.com))
分布式事务场景:
① 跨库场景
② 分库分表
③ 微服务化
像在微服务中,假如我们有这么几个模块:订单系统、库存系统和支付系统,如果这几个模块部署在同一个JVM上,那么我们可以使用本地事务来保证事务的原子性,但如果我们把不同的系统部署到不同的机子上,就必须通过分布式事务来保证事务特性。主要包括以下几种:
两阶段提交 2PC(two phase commit)(应用于DB层面)(阻塞式 要等所有的参与者执行完本地事务后统一提交,不能单个事务先提交)
2PC总结:管理者向每个参与者发送prepare请求,参与者受到请求后在本地执行事务,每个参与者执行完事务后会给管理者发送执行成功的消息,如果全部的参与者都执行成功,就给参与者发送提交消息,这个时候参与者才提交事务,然后释放锁资源。
2PC 即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2 是指两个阶段,P 是指准备阶段,C 是指提交阶段。
整个事务过程由事务管理器和参与者组成。
a. 准备阶段(Prepare phase):事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)
b. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
三阶段提交
三阶段总结:相对于2PC,三阶段多增加了一个询问的过程,管理者首先向所有参与者发送Cancommit请求,询问参与者是否可以执行该事务,参与者会返回yes或no。如果参与者返回yes,那么管理者会向参与者发送PreCommit请求,此时相当于2PC的第一阶段,执行成功后,参与者会向管理者发送ACK应答,然后发送doCommit执行,这时候参与者提交事务;或者在一开始参与者返回No,管理者会向参与者发送Abort请求,参与者中断事务;或是在第二步,有参与者没有返回Ack,或是返回了一个NO,这时候就回滚事务。
作为2PC的改进版,3PC将原有的两阶段过程,重新划分为CanCommit、PreCommit和doCommit三个阶段。
3PC 协议将 2PC 协议的准备阶段一分为二,从而形成了三个阶段:
所谓的三个阶段分别是:询问,然后再锁资源,最后真正提交。
第一阶段:CanCommit
① 事务询问。协调者向所有参与者发送包含事务内容的canCommit的请求,询问是否可以执行事务提交,并等待应答;
② 各参与者反馈事务询问。正常情况下,如果参与者认为可以顺利执行事务,则返回Yes,否则返回No。
第二阶段:PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的PreCommit操作。有以下两种可能:
- 执行事务预提交
- 中断事务
执行事务预提交
- 发送预提交请求。协调者向所有节点发出PreCommit请求,并进入prepared阶段;
- 事务预提交。参与者收到PreCommit请求后,会开始事务操作,并将Undo和Redo日志写入本机事务日志;
- 各参与者成功执行事务操作,同时将反馈以Ack响应形式发送给协调者,同事等待最终的Commit或Abort指令。
中断事务
如果任意一个参与者向协调者发送No响应,或者等待超时,协调者在没有得到所有参与者响应时,即可以中断事务。中断事务的操作为:
- 发送中断请求。 协调者向所有参与者发送Abort请求;
- 中断事务。无论是participant 收到协调者的Abort请求,还是participant 等待协调者请求过程中出现超时,参与者都会中断事务;
第三阶段:Do Commit
在这个阶段,会真正的进行事务提交,同样存在两种可能。
- 执行提交
- 回滚事务
执行提交
- coordinator发送提交请求。假如coordinator协调者收到了所有参与者的Ack响应,那么将从预提交转换到提交状态,并向所有参与者,发送doCommit请求;
- 事务提交。参与者收到doCommit请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源;
- 反馈事务提交结果。参与者将在完成事务提交后,向协调者发送Ack消息;
- 完成事务。协调者接收到所有参与者的Ack消息后,完成事务。
回滚事务
在该阶段,假设正常状态的协调者接收到任一个参与者发送的No响应,或在超时时间内,仍旧没收到反馈消息,就会回滚事务:
发送中断请求。协调者向所有的参与者发送rollback请求;
事务回滚。参与者收到rollback请求后,会利用阶段二中的Undo消息执行事务回滚,并在完成回滚后释放占用资源;
反馈事务回滚结果。参与者在完成回滚后向协调者发送Ack消息;
回滚事务。协调者接收到所有参与者反馈的Ack消息后,完成事务回滚。
TCC(采用补偿方法进行回滚,考验程序员的算法能力)(基于业务层面,要编写业务逻辑实现)补偿事务
针对每个任务都要注册与之对应的确认(Try)和补偿(Cancel)
TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销 Cancel。Try 操作做业务检查及资源预留,Confirm 做业务确认操作,Cancel 实现一个与 Try 相反的操作即回滚操作。TM 首先发起所有的分支事务的 Try 操作,任何一个分支事务的Try操作执行失败,TM 将会发起所有分支事务的 Cancel 操作,若 Try 操作全部成功,TM 将会发起所有分支事务的 Confirm 操作,其中 Confirm/Cancel 操作若执行失败,TM 会进行重试。
TCC 分为三个阶段:
- Try 阶段是做完业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的 Confirm 一起才能真正构成一个完整的业务逻辑。
- Confirm 阶段是做确认提交,Try 阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用 TCC 则认为 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。若 Confirm 阶段真的出错了,需引入重试机制或人工处理。
- Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用 TCC 则认为 Cancel 阶段也是一定成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工处理。
可靠消息服务
通过消息中间件实现,在A系统操作账户之前,首先向消息中间件发送一条消息,消息中间件收到这条消息后会将其持久化,但是不会进行投递,所以下游的系统不知道这条消息的存在,消息中间件持久化成功后,会向A系统发送一个确认应答。A系统收到确认应答后,就可以进行本地事务操作,A系统处理完成后,向消息中间件发送commit请求。对A系统来说,该事务的处理过程结束。消息中间件收到commit指令后,便向B系统投递该消息,从而触发B事务的执行。当B系统操作完成后,向中间件发送一个确认应答,告诉中间件该消息已经成功消费。分布式事务完成。
若事务A处理时失败,就会向中间件发送rollback请求,发完之后中间件直接将消息丢弃,不会触发B系统的任务
AT(alicloud seata用到了,回滚时自动补偿)