深入分析分布式柔性事务

ACID

什么是ACID?

原子性(Atomicity )

一个事务中所有操作都必须全部完成,要么全部不完成

一致性( Consistency )

在操作过程中不会破坏数据的完整性

拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元

隔离性或独立性( Isolation)

事务与事务之间不会互相影响,事务将假定只有它自己在操作数据库,其它事务不知晓

持久性(Durabilily)

一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此

简称就是ACID

ACID是如何保证单机事务,比如DB突然断电如何保证数据一致性?

使用SQL Server来举例, SQL Server 数据库是由两个文件组成的,一个数据库文件和一个日志文件,通常情况下,日志文件都要比数据库文件大很多。数据库进行任何写入操作的时候都是要先写日志的,同样的道理,我们在执行事务的时候数据库首先会记录下这个事务的redo操作日志,然后才开始真正操作数据库,在操作之前首先会把日志文件写入磁盘,那么当突然断电的时候,即使操作没有完成,在重新启动数据库时候,数据库会根据当前数据的情况进行undo回滚或者是redo前滚,这样就保证了数据的强一致性

CAP定理

ACID是针对单库下保证事务的理论,如果是多库即分布式下ACID将没有能力,这时就要使用CAP

什么是CAP定理?

什么是CAP?CAP原则或者叫CAP定理,都是指他

如果服务要求高可用性就需要采用分布式模式,来冗余数据写多份,写多份就会带来一致性问题,一致性问题又会带来性能问题,那么就此陷入了无解的死循环;所以只能取之中两个。

一致性(Consistency) :数据是一致更新的,所有数据节点的变动都是同步的,同时发生,同时生效

根据一致性的强弱程度不同,可以将一致性级别 5 种,参考:zookeeper.noteZookeeper 强一致性

可用性(Availability) :性能好+可靠性,在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求

分区容错性(Partition tolerance) :系统可以跨网络分区线性的伸缩和扩展,一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。

分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的故障,必须就当前操作在C和A之间做出选择

该理论已被证明:任何分布式系统只可同时满足两点,无法三者兼顾;所以应该根据应用场景进行适当取舍

当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。(分区容错性差)

提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致生效的。(分区容错性好,一致性就差)

要保证一致,每次写操作就都要等待全部节点写成功,

而这等待又会带来可用性的问题,即效率不好,要等嘛。(分区容错性好,一致性好,可用性就差)

总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低

注意:在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。原因已在上面说明了

Zookeeper 保证的是CP ,对于服务发现而言,可用性比数据一致性更加重要,而 Eureka 设计则遵循AP原则

BASE理论

牺牲CAP定理中所说的高一致性,获得可用性

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的

什么是BASE理论?

Basically Available(基本业务可用性(支持分区失败))

Soft state(软状态,状态允许有短时间不同步,异步)

Eventuallyconsistent(最终一致性(最终数据是一致的,但不是实时一致))

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)

酸碱平衡(ACID-BASEBalance)

真实系统应当是ACID与BASE的混合体,因此真实系统应当是酸碱平衡的

DTP模型——暂未做细节了解

X/Open DTP(X/OpenDistributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准

什么是分布式事务?

简单理解,多机事务

多数据源事务,对应多个 DB / MQ 操作需要保持一致的事务

分布式事务的重要性

随着微服务分布式架构的使用普及,分布式事务越来越成为一个绕不过去的问题,只要系统使用分布式架构,分布式事务问题或迟或早它就会存在

分布式事务解决方案

两阶段提交(2PC)

优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域

缺点:实现复杂,牺牲了可用性(牺牲了一部分可用性来换取的一致性),对性能影响较大,不适合高并发高性能场景

补偿事务(TCC)

优点:跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

缺点:缺点还是比较明显的,在事务发起方远程调用事务参与方的confirm、cancel方法中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理

本地消息表(异步确保)——遵循BASE理论,采用的是最终一致性

也就是使用独立消息服务 + MQ实现最终消息一致性,这种实现方式应该是业界使用最多的

这个方案即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),

也不会像TCC那样可能出现确认或者回滚不了的情况

优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性

缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理

什么是刚性事务,什么是柔性事务?

刚性事务

刚性事务满足 ACID 理论

工作在数据资源层,也就是比如数据库,MQ中间件

实现方式以 xa 2pc为代表

柔性事务

柔性事务满足 BASE 理论,基本上是可用的,数据最终会一致

工作在数据业务层,也就是比如应用服务、代码层面上

实现方式以可靠消息最终一致性异步确保型最大努力通知型TCC补偿型为代表

刚性事务的优缺点、特点及适用场景

特点

属于标准的分布式解决方案,全局事务提交型,

XA 2PC两阶段提交这些方式都会在事务整个过程全局锁定资源,对资源占用大

优点

高强度保证 ACID

缺点

效率低,也就是性能低,全局资源的锁定,造成所有参与的数据资源锁定状态贯穿整个事务过程,对资源的占用时间跨度大,成本高,

另外对资源有要求,数据源只有实现了XA规范才能接入,目前并不是所有的资源都支持XA规范

柔性事务的优缺点、特点及适用场景

特点

属于用代码控制处理逻辑的方式来保证分布式事务

可靠消息最终一致性异步确保型最大努力通知型 TCC补偿型这些方式基本使用定期询问、检查、重发、小粒度锁等方式实现分布式事务,对资源占用做到最低

优点

对资源占用低

缺点

为了实现松弛的约束下事务一致,需要添加多种子系统用于定期询问、检查、重发等工作,整个系统体积会增大

柔性事务

柔性事务的服务模式

服务模式属于在各种分布式事务解决方案中需要实现的一些能力,比如可查询操作表示要提供查询接口,幂等操作表示要实现幂等性支持重复消费下不影响业务数据

可查询操作

幂等操作

TCC操作

两阶段型操作!= 两阶段提交协议2PC操作

TCC也属于一种两阶段型操作

可补偿操作

柔性事务的解决方案

***可靠消息最终一致(异步确保型)——遵循BASE理论

PS:其实很多分布式事务场景可以通过这种方式实现事务上的最终一致

消息中间件的作用很多,例如异步通讯、解耦、并发缓冲、流量消峰等,但是传统的使用方式下消息中间件是不可靠的,原因是发送方 MQ 监听方三者是通过网络连接的,有网络的地方就是不稳定的。MQ 集群只是解决 MQ 组件自身的高可用,降低消息进来后消息丢失的风险。

问题描述

MQ 是网络连接的,不可靠,业务操作成功后一定要保证消息发出去,并且给对方消费,否则就属于事务不一致

两个应用系统 A、B,B 通过 MQ 消费 A 发送的消息

假设 A 是订单系统处理完自己的业务后,数据存储到 A-DB 然后发送消息到 MQ 让会计系统 B 联动记录会计凭证

又假设 A 做完订单业务后,发送 MQ 时因为网络问题没发成功或者 MQ 持久化出问题等原因消息丢了,

那么这一订单就丢失了会计凭证数据,A 和 B属于事务不一致,这是不允许发生的事

有一种说法是先发送 MQ 消息,成功后回头再做业务,这问题就更大了,如果消息ok了,回头业务做失败了,就等于记录了会计凭证而没有实际订单业务,问题更加严重

方案流程(需要引入三个子系统消息状态确认子系统消息恢复子系统消息服务管理子系统)

先提一个设想,如果我们能先发一个预消息到 MQ,但是此时消息状态是暂存,只有状态为待发送才能投递到监听端去消费,回头等业务做成功了再修改 MQ 消息状态为待发送,这样 MQ 才支持投递消息到消费端去消费,这样确实可行。但目前开源的常用 MQ 产品没有这种功能,好像 RocketMQ 提供,没有去确认,开发难度大,因此使用自建独立消息服务方式进行实现

执行业务前发送预消息到消息服务做消息入库,此时消息状态为待发送

业务系统得到消息服务的正确返回后再做业务,业务做完就将业务状态走 dubbo 异步通信方式通知到消息服务,消息服务根据状态决定消息是发送MQ还是删除或只是标记消息状态,业务库不用记录发送消息业务的状态,这里存在的异常点是可能因为业务处理失败或者业务处理成功而因为网络原因没能成功将业务状态发送给消息服务还或者业务处理成功而调用消息服务发送业务状态时因消息服务处理时间长而读取超时等,都会造成消息服务对应的消息库中堆积大量的消息状态为待发送的消息,这个异常点的解决都由消息状态确认子系统在消息服务中定期查询消息库中超过指定时间并且消息状态不是消费成功的消息,再调用业务系统的查询接口验证消息对应的业务状态,此时如果业务成功,就同步消息库的业务状态同时发送MQ,此时消息状态为发送中,如果业务失败,根据业务决定是否保留消息,这里一定要考虑数据增长问题,提前做好拆分的设计,使用分区、分表等方式在数据量庞大的时候也能快速读写,不影响性能

如果消息服务受到业务系统的业务状态是成功,那么马上将对应消息发送给 MQ,以 RabbitMQ 为例,这里消息服务和 MQ 的交互可能遇上这几类问题:

1、消息直接无法到达MQ

比如网络断了就会引起,这时直接走时间阶梯等待后重发即可,如果出现 connectionclosed 错误,直接增加 connection数即可

connectionFactory.setChannelCacheSize(100);

2、消息已经发送到达MQ,但返回的时候出现异常

rabbitmq提供了确认ack机制,可以用来确认消息是否有返回,因此可以根据发送回执 ack 决定是否要时间阶梯重发

/*confirmcallback用来确认消息是否有送达消息队列/rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {

如果消息没有到 exchange,则 ack=false

如果消息到达 exchange,则 ack=true

如果发送时根本找不到 exchange,则会触发 returncallback 函数 if (!ack) { //调用时间阶梯重发逻辑 } else { //可以记录消息服务中的消息状态为投递成功,此时基本认为消息会被监听方消费掉,监听方拿到消息就

//auto 签收掉,如果消费失败,就寄希望于消息状态确认子系统下一次再将消息发送到 mq 中 } });/**若消息找不到对应的Exchange会触发 returncallback */rabbitTemplate.setReturnCallback((message, replyCode, replyText, tmpExchange,tmpRoutingKey) -> { try { Thread.sleep(Constants.ONE_SECOND); } catch(InterruptedException e) { e.printStackTrace(); } log.info("send messagefailed: " + replyCode + " " + replyText);

//等待一段时间,再发rabbitTemplate.send(message); });

3、消息送达后,消息服务自己挂了

可能消息发送到 MQ 成功后,正当回传给消息服务时,消息服务自己挂了,导致监听方可能消费成功而消息服务还认为发送失败处于待发送状态

这其实没有关系,反正监听方是幂等的,哪怕消息服务重发消息,也不会出问题,而且监听方消费成功后会直接跨调消息服务告诉这条消息消费成功,同样可以覆盖掉这个问题,并没有关系,不用处理

4、监听方消费失败

这就依靠于消息状态确认子系统重发消息解决就好了

然后讲 MQ监听方环节,监听方拿到消息就 auto 签收,MQ消息随即 remove,监听方调具体的消费服务,这里监听方相当于是一个 MQ 的监听网关,自身可以做更多的服务编排工作,具体消费调用后面的消费服务,消费服务处理成功后监听方反过来调用消息服务,标记消息成功消费,如果消费服务出了任何问题包括网络问题导致消费失败

都由消息恢复子系统定时将消息库中超时的并且状态为发送中的消息拿出来再丢到MQ中,等于是重发机制

每次重发要比之前的间隔更长,提供可配置的时间阶梯,这样更加有利于等待被动方系统故障恢复,消费服务一定要是幂等的,这样重复消费也没问题,如果消息超过重发次数就移到死亡队列表中,这里可以添加消息服务管理子系统查看死亡消息有多少,可能有些是因为消费方系统故障原因,Bug 修复后可以操作重发这些死亡消息,也可以针对某一类型的死亡消息批量重发等等,如果监听方这边有性能问题,可以考虑用redis记录消息状态,这样监听方校验消息是否消费成功就更高效

最后一点,业务方对消息服务dubbo发送业务状态需要使用异步方式,因为如果消息服务处理成功后因为网络返回超时,将会导致业务方事务回滚,这样就不一致了,注意预发送消息不能用异步,因为它不仅证明此时链路没问题,而且它在消费服务中的结果要为后面的业务服务的。综上所述业务方调消息放要使用dubbo的异步方式

这样就能实现在不改造困难大的 MQ 的前提下实现可靠消息最终一致,

缺点或者不足方面:各调度子系统周期触发间的数据不一致延迟,及使用了多个服务和数据库有不少的开发成本增加了系统的体积,但它是一劳永逸,复用的

注意点及优化建议

如果业务方和消费方是这种订单-快递的场景,调用方式就是1:1,消费方性能问题倒还好,如果压力大可考虑消息服务对应的库和消费方的消息库使用redis等内存库,如果用redis,特别注意,宁愿牺牲大部分性能也要配置 aof 持久化为 always ,最大程度保证数据不丢

消息服务对应的消息表一定要考虑数据增长问题,提前做好拆分的设计,使用分区、分表等方式在数据量庞大的时候也能快速读写,不影响性能

业务系统传递业务操作状态给消息服务时,一定要用异步方式,比如是 dubbo 协议就用 dubbo 异步通信

dubbo:referenceid="demoServicemy2"interface=“com.test.dubboser.ServiceDemo2”

如果消息监听消费服务中为了实现幂等而开销比较大的话,可以考虑在消费端数据库中也增加一份消息表,记录消息处理状态,消费时直接获取消息状态就能决定知否直接返回结果

各种消息子系统其实都是定时任务,调度,这里建议使用分布式定时框架技术

***最大努力通知(定期校对,通知型)

和可靠消息最终一致方案针对解决的问题一样,可靠消息最终一致方案最终数据会一致,最多只是存在时间上的延迟而已,最大努力通知方案的思想是尽可能的进行多次通知被动方系统,不保证被动方数据一定会一致,同时不管被动方数据是否一致反正主动方数据变更后就不动了,最多只是定期或者每天提供一份数据对账的途经,可能是 http 查询接口,可能是下载对账文件供被动方 ftp 拿下来对账,以便被动方同步数据,纠正不一致的错误数据

适合的场景

首先他不适合那种被动方对数据一致性有较高强度要求的场景,因为有可能通知不到被动方造成数据不一致

同时主动方系统一定要有数据标准的能力,因为两边出现差异时都是以主动方数据为准进行同步修复的

适合用在对于业务最终一致性的时间敏感度低的场景下,也就是说能忍受一段时间内可能一天的数据不一致,同时比较适合跨企业级的系统,因为不太可能进行大量改造添加组件来满足一致性的要求,银行日清对账文件就是这种方案的体现

方案流程

主动方处理完业务流程后通过服务 http 方式或者 MQ 通知被动方,如果失败再按照设置的时间阶梯型通知规则,N次失败后记录成死亡消息,同时主动方也要为被动方提供业务数据的查询接口或者每个周期后产生一个对账文件,提供给被动方用于数据不一致时进行校正,被动方也是一样要实现幂等

***TCC(两阶段型、补偿型)

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作

TCC 分别指的是 trying、confirming、canceling 三个阶段

Try是先把多个应用中的业务资源预留和锁定住,为后续的确认提交打下基础

Confirm 是将Try 操作中涉及的所有应用的所有锁定资源全部提交处理

Cancel 是将Try 操作中涉及的所有应用的所有锁定资源全部回滚处理

TRYING阶段只要完全执行成功,默认就是要高强度能保证CONFIRMING阶段不能出错,

也就是说要在业务逻辑层面在trying环节尽最大努力做到各种检查和锁定,以确保confirming不出问题

TCC 三个过程就像数据库对事务的处理过程,对应数据库的 lock、commit、rollback

适合的场景

TCC 方案对事务控制的特点是准实时的,适合用在对实时性要求较高的场景下

比如订单支付环节,用户一旦下单付款成功,订单系统的订单状态将马上显示支付完成,同时对应的账户系统的账户余额就要对应减少,不能出现太长的先后顺序,也不能允许因为任何环节出错而等待消息重发、对账等方式再进行数据同步修复,这样的话,这一段时间的数据将不一致,这种场景下是不能允许的

上面这种场景就不适合可靠消息最终一致性方案,使用 TCC 比较适合

TCC方案的优点 / 特点

对资源的锁定程度较少,尽可能的直接将需要的数据部分划出去锁定起来,这是软锁,与 XA 两阶段提交做全局锁的方案是不同的——这将直接在数据资源层面形成等待、阻塞,引起吞吐能力下降,造成系统性能不好

TCC事务的缺点

TCC 的Try、Confirm和Cancel操作功能需业务提供,并且开发成本高

TCC方案的原理——个人总结

TCC 实质上是应用层的2PC(2 PhaseCommit, 两阶段提交),好比把 XA 两阶段提交那种在数据资源层做的事务管理工作提到了数据应用层,每个应用可以看作一个资源管理器,他们的工作对应如下的描述

1、Try:尝试执行业务

完成所有业务检查(一致性)

预留锁定必须的业务资源(准隔离性)

2、Confirm:确认执行业务

不再做业务检查

记录成功,并只使用Try阶段预留锁定的业务资源做确认操作

默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定得成功,也就是只能通过在Try阶段进行强有力的逻辑控制,保障Confirm不出事

3、Cancel:取消执行业务

记录回滚,并释放Try阶段预留锁定的业务资源

一个完整的TCC事务参与方包括三部分

主业务服务

主业务服务为整个业务活动的发起方,如订单支付中订单支付系统属于主业务服务,其他的积分、账户都属于从业务服务

从业务服务

从业务服务负责提供TCC业务操作,是整个业务活动的操作方。从业务服务必须实现Try、Confirm和Cancel三个接口,供主业务服务调用。由于Confirm和Cancel操作可能被重复调用,故要求Confirm和Cancel两个接口必须是幂等的

业务活动管理器

业务活动管理器管理控制整个业务活动,包括记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态,在业务活动提交时确认所有的TCC型操作的confirm操作,

在业务活动取消时调用所有TCC型操作的cancel操作

整个TCC事务对于主业务服务来说是透明的,其中业务活动管理器和从业务服务各自干了一部分工作

一个案例理解

接下来将以账务拆分为例,对TCC事务的流程做一个描述,业务场景如下,

分别位于三个不同分库的帐户A、B、C,A和B一起向C转帐共80元:

1、Try:尝试执行业务

完成所有业务检查(一致性):检查A、B、C的帐户状态是否正常,帐户A的余额是否不少于30元,帐户B的余额是否不少于50元。

预留必须业务资源(准隔离性):帐户A的冻结金额增加30元,帐户B的冻结金额增加50元,这样就保证不会出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,帐户A和B的可用余额不够的情况。

2、Confirm:确认执行业务

真正执行业务:如果Try阶段帐户A、B、C状态正常,且帐户A、B余额够用,则执行帐户A给账户C转账30元、帐户B给账户C转账50元的转帐操作。

不做任何业务检查:这时已经不需要做业务检查,Try阶段已经完成了业务检查。

只使用Try阶段预留的业务资源:只需要使用Try阶段帐户A和帐户B冻结的金额即可。

3、Cancel:取消执行业务

释放Try阶段预留的业务资源:如果Try阶段部分成功,比如帐户A的余额够用,且冻结相应金额成功,帐户B的余额不够而冻结失败,则需要对帐户A做Cancel操作,将帐户A被冻结的金额解冻掉。

幂等性的实现方式

1、通过唯一键值做处理,即每次调用的时候传入唯一键值,通过唯一键值判断业务是否被操作,如果已被操作,则不再重复操作

2、通过状态机处理,给业务数据设置状态,通过业务状态判断是否需要重复执行

你可能感兴趣的:(架构,深入分析分布式柔性事务)