一 CAP理论
1.1 CAP理论
CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这3个基本需求,最多只能同时满足其中的2个。
选项 | 描述 |
---|---|
Consistency(一致性) | 指数据在多个副本之间能够保持一致的特性(严格的一致性) |
Availability(可用性) | 指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据) |
Partition tolerance(分区容错性) | 分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障 |
1.2 CAP案例场景
假如现在有这样的场景:
-
用户访问了N1,修改了D1的数据。
-
用户再次访问,请求落在了N2。此时D1和D2的数据不一致。
接下来:
-
保证
一致性
:此时D1和D2数据不一致,要保证一致性就不能返回不一致的数据,可用性
无法保证。 -
保证
可用性
:立即响应,可用性得到了保证,但是此时响应的数据和D1不一致,一致性
无法保证。
所以,可以看出,分区容错的前提下,一致性
和可用性
是矛盾的。
1.3 对应的模型应用
-
ZooKeeper 保证的是 CP
-
Eureka 保证的则是 AP
-
Nacos 不仅支持 CP 也支持 AP
二 分布式锁
2.1 分布式锁的实现方式
常见的分布式锁实现方案有三种:MySQL分布式锁
、ZooKepper分布式锁
、Redis分布式锁
2.1.1 MySQL分布式锁
数据库悲观锁实现的分布式锁:可以使用 select ... for update 来实现分布式锁。
用数据库实现分布式锁比较简单,就是创建一张锁表,数据库对字段作唯一性约束。加锁的时候,在锁表中增加一条记录即可;释放锁的时候删除记录就行。
如果有并发请求同时提交到数据库,数据库会保证只有一个请求能够得到锁。
这种属于数据库 IO 操作,效率不高,而且频繁操作会增大数据库的开销,因此这种方式在高并发、高性能的场景中用的不多
2.1.2 基于zk实现分布式锁
ZooKeeper的数据节点和文件目录类似,例如有一个lock节点,在此节点下建立子节点是可以保证先后顺序的,即便是两个进程同时申请新建节点,也会按照先后顺序建立两个节点。
2.1.3 基于redis实现分布式锁
基于 Redis 分布式锁一般有以下这几种实现方式:setnx+expire。
加锁了之后如果机器宕机,那我这个锁就无法释放,所以需要加入过期时间,而且过期时间需要和setNx同一个原子操作,在Redis2.8之前需要用lua脚本,但是redis2.8之后redis支持nx和ex操作是同一原子操作。
三 分布式事务
3.1 分布式事务
1.首先满足事务特性:ACID
2.而在分布式环境下,会涉及到多个数据库
分布式事务处理的关键是:
1.需要记录事务在任何节点所做的所有动作;
2.事务进行的所有操作要么全部提交,要么全部回滚。
目的是为了保证分布式系统中的数据一致性。
四 方案1:2pc
4.1 分布式事务2PC流程
2PC,两阶段提交,将事务的提交过程分为资源准备和资源提交两个阶段,并且由TC事务协调者来协调所有事务参与者,如果准备阶段所有事务参与者都预留资源成功,则进行第二阶段的资源提交,否则事务协调者回滚资源。
4.2 第一阶段:准备阶段
由事务协调者询问通知各个事务参与者,是否准备好了执行事务:
1.协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
2.各参与者执行本地事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
3.如参与者执行成功,给协调者反馈同意,否则反馈中止,表示事务不可以执行。
流程如下:
4.3 第二阶段:提交阶段
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit提交或者rollback回滚
4.3.1 如果正常提交阶段
当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,具体流程如下:
1. 协调者节点向所有参与者节点发出正式提交的 commit 请求。
2. 收到协调者的 commit 请求后,参与者正式执行事务提交操作,并释放在整个事务期间内占用的资源。
3. 参与者完成事务提交后,向协调者节点发送ACK消息。
4. 协调者节点收到所有参与者节点反馈的ACK消息后,完成事务。
正常提交时,事务的完整流程如下:
4.3.2 如果异常回滚阶段
如果任意一个参与者节点在第一阶段返回的消息为中止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,具体流程如下:
1.协调者向所有参与者发出 rollback 回滚操作的请求
2.参与者利用阶段一写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
3. 参与者在完成事务回滚之后,向协调者发送回滚完成的ACK消息
4.协调者收到所有参与者反馈的ACK消息后,取消事务
事务回滚,流程如下:
4.4 分布式2pc的优点与缺点
优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。
缺点: 二阶段提交确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的
1.性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景
2.可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)。
3.数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
2.2.2 方案2:3PC
3pc分为3个阶段:CanCommit
,PreCommit
,DoCommit
三个阶段。
1.CanCommit:准备阶段。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
2.PreCommit:预提交阶段。协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务,参与者执行完操作之后返回ACK响应,同时开始等待最终指令。
3.DoCommit:提交阶段。协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务:
3.1 如果所有参与者都返回正确的ACK
响应,则提交事务
3.2 如果参与者有一个或多个参与者收到错误的ACK
响应或者超时,则中断事务
3.3 如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,在等待超时之后,会继续进行事务提交
总结:三阶段提交解决的只是两阶段提交中单体故障和同步阻塞的问题,因为加入了超时机制,这里的超时的机制作用于 预提交阶段 和 提交阶段。如果等待 预提交请求 超时,参与者直接回到准备阶段之前。提交阶段: 如果等到提交请求超时,那参与者就会提交事务了。
无论是2PC还是3PC都不能保证分布式系统中的数据100%一致。
2.2.3 方案3:TCC
基于TCC实现的分布式事务也可以看做是对业务的一种补偿机制。TCC 是业务层面的分布式事务,保证最终一致性,不会一直持有资源的锁。
1.Try:尝试待执行的业务。订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,。
2.Confirm:确认执行业务,如果Try阶段执行成功,接着执行Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
3.Cancel:取消待执行的业务,如果Try阶段执行失败,执行Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
3.1优点: 把数据库层的二阶段提交交给应用层来实现,规避了数据库的 2PC 性能低下问题
3.2缺点:TCC 的 Try、Confirm 和 Cancel 操作功能需业务提供,开发成本高。TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
2.2.4 方案4:本地消息表
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
思想: 可以在订单库新增一个消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到MQ,库存服务去消费MQ。
执行流程:
-
订单服务,添加一条订单和一条消息,在一个事务里提交
-
订单服务,使用定时任务轮询查询状态为未同步的消息表,发送到MQ,如果发送失败,就重试发送
-
库存服务,接收MQ消息,修改库存表,需要保证幂等操作
-
如果修改成功,调用rpc接口修改订单系统消息表的状态为已完成或者直接删除这条消息
-
如果修改失败,可以不做处理,等待重试。
本地消息表这种方案实现了最终一致性,需要在业务系统里增加消息表,业务逻辑中多一次插入的DB操作,所以性能会有损耗,而且最终一致性的间隔主要有定时任务的间隔时间决定。
2.2.5 方案5:MQ消息事务
消息事务的原理是将两个事务通过消息中间件进行异步解耦。
执行流程:
-
发送prepare消息到消息中间件
-
发送成功后,执行本地事务
-
如果事务执行成功,则commit,消息中间件将消息下发至消费端
-
如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
-
消费端接收到消息进行消费,如果消费失败,则不断重试
优点与缺点
消息事务依赖于消息中间件的事务消息,例如我们熟悉的RocketMQ就支持事务消息(半消息),也就是只有收到发送方确定才会正常投递的消息。https://mp.weixin.qq.com/s/qPUnyjXOe-4bwvwJ-PPe7A
这种方案也是实现了最终一致性,对比本地消息表实现方案,不需要再建消息表,对性能的损耗和业务的入侵更小。
2.2.6 方案6:最大努力通知
最大努力通知相比实现会简单一些,适用于一些对最终一致性实时性要求没那么高的业务,比如支付通知,短信通知。
执行流程:
-
业务系统调用支付平台支付接口, 并在本地进行记录,支付状态为支付中
-
支付平台进行支付操作之后,无论成功还是失败,同步给业务系统一个结果通知
-
如果通知一直失败则根据重试规则异步进行重试,达到最大通知次数后,不再通知
-
支付平台提供查询订单支付操作结果接口
-
业务系统根据一定业务规则去支付平台查询支付结果
2.2.7 方案7:分布式方案seata
1.seata是一款解决分布式事务的解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
2.seata的几种术语:
一个中心:全局事务id
TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
3.一个典型的分布式事务过程:
流程:
1.服务A中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID
2.服务A中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
3.服务A开始执行分支事务
4.服务A开始远程调用B服务,此时 XID 会根据调用链传播
5.服务B中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
6.服务B开始执行分支事务
7.全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚
8.TC协调其管辖之下的所有分支事务,决定是提交还是回滚