赞
踩
分布式事务是指在分布式环境下的事务处理,其目的是保证在多个节点上的操作要么全部执行,要么全部回滚,以确保数据的一致性。
因为分布式环境中存在网络分区、服务器宕机等问题,所以分布式事务处理比单机事务处理更加复杂且难以实现。
这样,分布式订单超时机制就可以在分布式环境下简单有效地维护订单的超时状态。
事务必须作为一个不可分割的工作单元,要么全部执行,要么全部不执行。
一致性是指数据处于一种语义上的有意义且正确的状态。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其他事务不可见的。因为这些中间状态,是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。
举个例子,张三给李四转账100元。事务要做的是从张三账户上减掉100元,李四账户上加上100元。一致性的含义是其他事务要么看到张三还没有给李四转账的状态,要么张三已经成功转账给李四的状态,而对于张三少了100元,李四还没加上100元这个中间状态是不可见的。
我们来看一下转账过程中可能存在的状态:
张三已扣减、李四未收到
上述过程中: 1.是初始状态、2是中间状态、3是最终状态,1和3是我们期待的状态,但是2这种状态却不是我们期待出现的状态。-锁
那么反驳的声音来了:
要么转账操作全部成功,要么全部失败,这是原子性
。从例子上看全部成功,那么一致性就是原子性的一部分咯,为什么还要单独说一致性
和原子性
?
你说的不对。在未提交读的隔离级别下是事务内部操作是可见的,明显违背了一致性,怎么解释?
好吧,需要注意的是:
原子性和一致性的的侧重点不同︰ 原子性关注状态
,要么全部成功,要么全部失败,不存在部分成功的状态。而一致性关注数据的可见性
,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
隔离性是多个事务的时候,相互不能干扰,一致性是要保证操作前和操作后数据或者数据结构的一致性,而我提到的事务的一致性是关注数据的中间状态,也就是一致性需要监视中间状态的数据,如果有变化,即刻回滚
如果不考虑隔离性,事务存在3种并发访问数据问题,也就是事务里面的脏读
、不可重复读
、虚度/幻读
mysql的隔离级别:读未提交、读已提交、可重复读、串行化
是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
对于分布式事务而言几乎满足不了ACID,其实对于单机事务而言大部分情况下也没有满足ACID,不然怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应用上的分布式事务了。
网络分区: 当分布式系统的部分节点与其他节点网络隔离时,这些隔离的节点将无法与其他节点通信,从而导致数据不一致。
节点宕机: 如果节点宕机,分布式系统可能无法正常工作,从而导致数据不一致。
数据丢失: 如果在数据传输过程中发生数据丢失,分布式系统中的数据将不一致。
数据冲突: 在分布式系统中,如果多个节点同时对同一数据进行更新,数据冲突将可能导致数据不一致。
时间不同步: 如果分布式系统中的节点的时钟不同步,将导致分布式系统中的数据不一致。
这些故障是分布式系统中常见的数据不一致的原因,为了确保数据的一致性,开发人员需要采用相应的技术和策略来防范这些故障。
举例:
cap理论是分布式系统的理论基石
“all nodes see the same data at the same time",即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
可用性指"Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
如果你你是一个分布式系统,那么你必须要满足一点:分区容错性
CAP三个特性只能满足其中两个,那么取舍的策略就共有三种:
CA without P: 如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
CP without A: 如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
AP wihtout C: 要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。
分布式系统中的一致性是弱一致性单数据库mysql的一致性强一致性
BASE是Basically Available(基本可用)、Soft state(软状态) 和Eventually consistent(最终一致性) 三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性―-注意,这绝不等价于系统不可用。比如:
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。
因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
假设有一个分布式银行系统,其中张三和李四的账户都存在于不同的节点上。在这种情况下,如果张三向李四转账100元,那么就需要执行一次分布式事务。
在BASE定理的框架下,分布式事务可以保证“基本可用”,即交易可以在整个系统中进行,但不能保证它是立即一致的。在这种情况下,在事务最终完成之前,可能会有暂时的不一致性。但是,在一段时间后,系统将回到一致状态。
因此,在使用BASE定理的分布式系统中,张三向李四转账100元的事务可以执行,但不能保证立即生效,因为需要一段时间才能完全一致。
一句话:CAP就是告诉你:想要满足C、A、P就是做梦,BASE才是你最终的归宿
常见分布式事务解决方案:
两阶段提交又称2PC,2PC是一个非常经典的中心化的原子提交协议。
这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant) 。
两个阶段︰第一阶段:投票阶段和第二阶段:提交/执行阶段。
举例:订单服务A,需要调用支付服务B去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。
那么看2PC阶段是如何处理的
第一阶段主要分为3步
第一阶段执行完后,会有两种可能。1、所有都返回Yes.2、有一个或者多个返回No。
成功条件︰ 所有参与者都返回Yes。
第二阶段主要分为两步
异常条件︰ 任何一个参与者向协调者反馈了No响应,或者等待超时之后,协调者尚未收到所有参与者的反馈响应。
异常流程第二阶段也分为两步:
通过上面的演示,很容易想到2pc所带来的缺陷
无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,
参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于
锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
由于协调者无法收集到所有参与者的反馈,会陷入阻塞情况。
解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。
无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.
解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
根据以上情况举例:
这张图刚开始理解会很难 下面逐步分析后再回来理解
一个订单支付之后,我们需要做下面的步骤:
好,业务场景有了,现在我们要更进一步,实现一个TCC分布式事务的效果。
什么意思呢?也就是说:
上述这几个步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。
举个例子,现在订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是100件,现在卖掉了2件,本来应该是98件了。
结果呢?由于库存服务操作数据库异常,导致库存数量还是100。这不是在坑人么,当然不能允许这种情况发生了!
但是如果你不用TCC分布式事务方案的话,就用个go开发这么一个微服务系统,很有可能会干出这种事来
我们来看看下面的这个图,直观的表达了上述的过程:
所以说,我们有必要使用TCC分布式事务机制来保证各个服务形成一个整体性的事务。
上面那几个步骤,要么全部成功,如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。
比如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停止执行增加积分和通知出库两个操作。
说了那么多,老规矩,给大家上一张图,大伙儿顺着图来直观的感受一下:
那么现在到底要如何来实现一个TCC分布式事务,使得各个服务,要么一起成功?要么一起失败呢?
大家稍安勿躁,我们这就来一步一步的分析一下。咱们就以一个go开发系统作为背景来解释。
首先,订单服务那儿,它的代码大致来说应该是这样子的:
Go语言演示:
type OrderService struct{
creditSrvClient proto.CreditClient //用户积分
wmsSrlvclient proto.wmsClient //记录仓库的变动信息
InventorySrvclient proto.InventoryClient //库存确认扣减
}
func New0rderService( ) *OrderService{
return &OrderService{
CreditSrvclient: proto.Creditclient{},
wmsSrvclient: proto.wmsClient{},
InventorySrvClient: proto.InventoryClient{},
}
}
func (o OrderService) UpdateOrderStatus ( ) error {
return nil
}
func (o OrderService)Notify( ) error {
o.UpdateOrderStatus()//更新订单的状态
o.CreditSrvClient.AddCredit( )//增加积分
o.InventorySrvClient.ReduceStock( ) //1库存确认扣减
o.wmsClient.SaleDelivery()//记录仓库变更记录
return nil
}
其实就是订单服务完成本地数据库操作之后,通过grpc
来调用其他的各个服务罢了。
但是光是凭借这段代码,是不足以实现TCC分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点儿代码好不好。
首先,上面那个订单服务先把自己的状态修改为:TRADE_SUCCESS
。
这是啥意思呢?也就是说,在 pay()
那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为UPDATING
,也就是修改中的意思。
这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。
然后呢,库存服务直接提供的那个reduce_stock()
接口里,也别直接扣减库存啊,你可以是冻结掉库存。
举个例子,本来你的库存数量是100,你别直接100 - 2=98
,扣减这个库存!
你可以把可销售的库存:100 -2= 98
,设置为98
没问题,然后在一个单独的冻结库存的字段里,设置一个2。也就是说,有2个库存是给冻结了。
积分服务的add_credit()
接口也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。
比如:用户积分原本是1190
,现在要增加10
个积分,别直接1190+10= 1200
个积分啊!
你可以保持积分为1190
不变,在一个预增加字段里,比如说prepare_add_credit
字段,设置一个10
,表示有10
个积分准备增加。
仓储服务的sale_delivery()
接口也是同理啊,你可以先创建一个销售出库单,但是这个销售出库单的状态是UNKNOWN
。
也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢!
上面这套改造接口的过程,其实就是所谓的TCC 分布式事务
中的第一个T字母代表的阶段,也就是Try阶段。
总结上述过程,如果你要实现一个TCC分布式事务
,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个Try的操作。
这个操作,一般都是锁定某个资源,设置一个预备类的状态,冻结部分数据,等等,大概都是这类操作。咱们来一起看看下面这张图,结合上面的文字,再来捋一捋整个过程:
然后就分成两种情况了,第一种情况是比较理想的,那就是各个服务执行自己的那个Try
操作,都执行成功了,Bingo!
这个时候,就需要依靠TCC分布式事务框架来推动后续的执行了。这里简单提一句,如果你要玩儿TCC分布式事务,必须引入一款TCC分布式事务框架,比如java国内开源的seata
、ByteTCC
、Himly
、TCC-transaction
。
否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了。
如果你在各个服务里引入了一个TCC分布式事务的框架,订单服务里内嵌的那个TCC分布式事务框架可以感知到,各个服务的Try操作都成功了。
此时,TCC 分布式事务框架会控制进入TCC下一个阶段,第一个C阶段,也就是Confirm阶段。为了实现这个阶段,你需要在各个服务里再加入一些代码。比如说,订单服务里,你可以加入一个Confirm的逻辑,就是正式把订单的状态设置为“已支付”了,大概是类似下面这样子:
func (o OrderService) Pay ( ) error {
gorm.UpdateStatus ( "TRADE_SUCCESS")
}
库存服务也是类似的,你可以有一个InventoryServiceConfirm
类,里面提供一个reduce_stock()
接口的Confirm
逻辑,这里就是将之前冻结库存字段的2
个库存扣掉变为0
。
这样的话,可销售库存之前就已经变为98了,现在冻结的2个库存也没了,那就正式完成了库存的扣减。
积分服务也是类似的,可以在积分服务里提供一个CreditServiceConfirm
类,里面有一个addCredit()
接口的Confirm
逻辑,就是将预增加字段的10
个积分扣掉,然后加入实际的会员积分字段中,从1190
变为1120
。
仓储服务也是类似,可以在仓储服务中提供一个WmsServiceConfirm
类,提供一个sale_delivery()
接口的Confirm
逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理人员查看和使用,而不是停留在之前的中间状态UNKNOWN
了。
好了,上面各种服务的Confirm
的逻辑都实现好了,一旦订单服务里面的TCC分布式事务框架感知到各个服务的Try
阶段都成功了以后,就会执行各个服务的Confirm
逻辑。
订单服务内的TCC事务框架会负责跟其他各个服务内的TCC事务框架进行通信,依次调用各个服务的Confirm逻辑。然后,正式完成各个服务的所有业务逻辑的执行。
同样,给大家来一张图,顺着图一起来看看整个过程:
好,这是比较正常的一种情况,那如果是异常的一种情况呢?
举个例子:在Try
阶段,比如积分服务吧,它执行出错了,此时会怎么样?
那订单服务内的TCC事务框架是可以感知到的,然后它会决定对整个TCC分布式事务进行回滚。
也就是说,会执行各个服务的第二个C阶段,Cancel阶段。同样,为了实现这个Cancel 阶段,各个服务还得加一些代码。
首先订单服务,它得提供一个OrderServiceCancel 的类,在里面有一个pay()接口的Cancel逻辑,就是可以将订单的状态设置为CANCELED
,也就是这个订单的状态是已取消。
库存服务也是同理,可以提供reduce_stock()
的Cancel逻辑,就是将冻结库存扣减掉2,加回到可销售库存里去,98+2= 100
。
积分服务也需要提供addCredit()
接口的Cancel逻辑,将预增加积分字段的10个积分扣减掉。
仓储服务也需要提供一个sale_delivery()
接口的Cancel逻辑,将销售出库单的状态修改为“CANCELED"设置为已取消。
然后这个时候,订单服务的TCC分布式事务框架只要感知到了任何一个服务的Try逻辑失败了,就会跟各个服务内的TCC 分布式事务框架进行通信,然后调用各个服务的Cancel逻辑。
大家看看下面的图,直观的感受一下:
总结一下,你要玩儿TCC分布式事务的话:
如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?
TCC事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。
万一某个服务的Cancel或者Confirm逻辑执行一直失败怎么办呢?那也很简单,TCC事务框架会通过活动日志记录各个服务的状态。
举个例子,比如发现某个服务的Cancel或者Confirm一直没成功,会不停的重试调用他的Cancel或者Confirm逻辑,务必要他成功!
当然了,如果你的代码没有写什么bug,有充足的测试,而且Try阶段都基本尝试了一下,那么其实一般Confirm、Cancel都是可以成功的!
如果实在解决不了,那么这个一定是很小概率的事件,这个时候发邮件通知人工处理
seata、go-seata
优点:
缺点:
本地消息表方案
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明︰
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
交互流程如下∶
用户服务在本地事务新增用户和增加“积分消息日志”。(用户表和消息表通过本地事务保证一致)下表是伪代码
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一事务中,本地数据库操作与记录消息日志操作具备原子性。
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后消息中间件回应ack,否则消息中间件将重复投递此消息。
由于消息会重复投递,积分服务的“增加积分”功能需要实现幂等性。
消息不可靠
上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
RocketMQ是一个来自阿里巴巴的分布式消息中间件,于2012年开源,并在2017年正式成为Apache顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在RocketMQ之上,并且最近几年的双十一大促中,RocketMQ都有抢眼表现。
Apache RocketMQ4.3之后的版本正式支持事务消息,为分布式事务实现提供来便利性支持。
RocketMQ事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题,RocketMQ的设计中broker与producer端的双向通信能力,使得broker天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;
RocketMQ的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决Producer端的消息发送与本地事务执行的原子性问题。
执行流程如下︰
为方便理解我们还以注册送积分的例子来描述整个流程。
Producer即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预览状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已接收到消息。
Producer端执行业务代码逻辑,通过本地数据库事务控制。本例中,Producer执行添加用户操作。
若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到c
ommit消息后将“增加积分消息”状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer本地事务执行失败则自动向MQ Server发送rollback消息,MQ Server接收到rollback消息后将删除“增加积分消息”。
MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户则来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
最大努力通知也是一种解决分布式事务的方案,下边是一个是充值的例子:
目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
通过对最大努力通知的理解,采用MQ的ack机制就可以实现最大努力通知。
方案1:
流程如下:
如下图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。