前阵子从支付宝转账1万块钱到余额宝,这是日常生活的一件普通小事,但作为互联网研发人员的职业病,我就思考支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没有增加1万,数据就会出现不一致状况了。
上述场景在各个类型的系统中都能找到相似影子,比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?!
在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?!等等,相信大家或多或多少都能碰到相似情景。
本质上问题可以抽象为:当一个表数据操作成功后,怎么保证另一个表的数据也必须要操作成功。当然啦,这两个数据表不在一个数据源中
在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。
但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。
事务: 指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行.
本地事务: SqlSessionfactory --> 一个数据库范围类事务管理.
分布式事务:跨了多个数据库事务管理,在微服务架构每个服务都有自己数据库,在微服务架构中必然要用到分布式事务.
刚性事务是指严格遵循ACID原则的事务, 例如单机环境下的数据库事务.
柔性事务是指遵循BASE理论的事务, 通常用在分布式环境中, 常见的实现方式有:
①两阶段提交(2PC)
②TCC补偿型提交
③基于消息的异步确保型
④最大努力通知型
通常对本地事务采用刚性事务, 分布式事务使用柔性事务.
分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。
XA协议包含两阶段提交(2PC),这里我们重点介绍两阶段提交的具体过程
两阶段提交(Two Phase Commit, 2PC), 具有强一致性, 是系统的一种典型实现.
两阶段提交, 常见的标准是XA, JTA等. 例如Oracle的数据库支持XA.
在魔兽世界这款游戏中,副本组团打BOSS的时候,为了更方便队长与队员们之间的协作,队长可以发起一个“就位确认”的操作:
当队员收到就位确认提示后,如果已经就位,就选择“是”,如果还没就位,就选择“否”。
当队长收到了所有人的就位确认,就会向所有队员们发布消息,告诉他们开始打BOSS。
相应的,在队长发起就位确认的时候,有可能某些队员还并没有就位:
以上就是魔兽世界当中组团打BOSS的确认流程。这个流程和XA分布式事务协议的两阶段提交非常相似。
那么XA协议究竟是什么样子呢?在XA协议中包含着两个角色:事务协调者和事务参与者。让我们来看一看他们之间的交互流程:
在XA分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。
在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。
当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。
在XA分布式事务的第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。
接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。
当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。
第一阶段
第二阶段
在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。
于是在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。
以上就是XA两阶段提交协议的详细过程
注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到,则提交,如果则回滚。如果是,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在阶段Si就崩溃了,因此需要回滚。
现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)来快速实现。
不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?
XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
TCC事务的出现正是为了解决应用拆分带来的跨应用业务操作原子性的问题。当然,由于常规的XA事务(2PC,2 Phase Commit, 两阶段提交)性能上不尽如人意,也有通过TCC事务来解决数据库拆分的使用场景。
个参与者需要实现3个操作:Try、Confirm 和 Cancel,3个操作对应2个阶段,Try 方法是一阶段的资源检测和预留阶段,Confirm 和 Cancel 对应二阶段的提交和回滚。
图中,事务开启的时候,由发起方去触发一阶段的方法,然后根据各个参与者的返回状态,决定二阶段是调 Confirm 还是 Cancel 方法。
我们先套一个业务场景进去,如下图所示
那页面点了支付按钮,调用支付服务,那我们后台要实现下面三个步骤
[1] 订单服务-修改订单状态
[2] 账户服务-扣减金钱
[3] 库存服务-扣减库存
达到事务的效果,要么一起成功,要么一起失败!就要采取TCC分布式事务方案!
TCC又可以被称为两阶段补偿事务,第一阶段try只是预留资源,第二阶段要明确的告诉服务提供者,这个资源你到底要不要,对应第二阶段的confirm/cancel,用来清除第一阶段的影响,所以叫补偿型事务。
再打个比方,说TCC太高大上是吧,讲RM中的prepare、commit、rollback接口,总知道吧。可以类比的这么理解
那差别在哪呢?
rollback、commit、prepare,站在开发者层面是感知不到的,数据库帮你做了资源的操作!
而try、confirm、cancel,站在开发者层面是能感知到的,这三个方法的业务逻辑,即对资源的操作,开发者是要自己去实现的!
好,下面套入我们的场景,怎么做呢。比如,你的订单服务中本来只有一个接口
//修改代码状态
orderClient.updateStatus();
都要拆为三个接口,即
orderClient.tryUpateStatus();
orderClient.confirmUpateStatus();
orderClient.cancelUpateStatus();
注意了:面试官如果问你,TCC有什么缺点?这就是很严重的缺点,对代码入侵性大!每套业务逻辑、都要按try(请求资源)、confirm(操作资源)、cancel(取消资源),拆分为三个接口!
具体每个阶段,每个服务业务逻辑是什么样的呢?
假设,库存数量本来是50,那么可销售库存也是50。账户余额为50,可用余额也为50。用户下单,买了1个单价为1元的商品。流程如下:
Try阶段:
订单服务:修改订单的状态为【支付中】
账户服务:账户余额不变,可用余额减1,然后将1这个数字冻结在一个单独的字段里
库存服务:库存数量不变,可销售库存减1,然后将1这个数字冻结在一个单独的字段里
confirm阶段
订单服务:修改订单的状态为【支付完成】
账户服务:账户余额变为(当前值减冻结字段的值),可用余额不变(Try阶段减过了),冻结字段清0。
库存服务:库存变为(当前值减冻结字段的值),可销售库存不变(Try阶段减过了),冻结字段清0。
cancel阶段
订单服务:修改订单的状态为【未支付】
账户服务:账户余额不变,可用余额变为(当前值加冻结字段的值),冻结字段清0。
库存服务:库存不变,可销售库存变为(当前值加冻结字段的值),冻结字段清0。
伪代码
接下来从代码程序来说明,为了便于演示,将入参略去。
本来,你支付服务的代码是长下面这样的
那么,用上TCC模型后,代码变成下面这样
注意了,这种写法其实严格上来说,不是不行。看你业务场景,因为存在一些瑕疵,看你自己有没办法接受
(1)cancel或者confirm出现异常了,你怎么处理?
例如在cancel阶段执行如下三行代码
orderClient.cancelUpdateStatus();
accountClient.cancelDecrease();
repositoryClient.cancelDecrease();
你第二行出现异常了,第三行没跑就退出了,怎么办?你要对此进行业务补偿!
(2)大量逻辑重复
你看啊,我们的执行架构其实是这样的
try{
xxclient.try();
}catch(Throwable t){
xxclient.cancel();
throw t;
}
xxclient.confirm();
有没办法让这个架子交给框架去执行,我们告诉框架,你在每个阶段要执行哪些方法就好!
因此,需要引入TCC分布式事务框架,事务的Try、Confirm、Cancel三个状态交给框架来感知!你只要告诉框架,Try要执行啥,Confirm要执行啥,Cancel要执行啥!如果Cancel过程出现异常了,框架有内部的补偿措施给你恢复数据!
以分布式tcc框架hmily
为例,如果出现cancel异常或者confirm异常的情况,在try阶段会保存好日志,Hmily有内置的调度线程池来进行恢复,不用担心。
那hmily,怎么感知状态的呢?也很简单,就是切面编程,核心逻辑如下几行
我们在使用过程中,只要通过@Tcc注解告诉框架confirm方法执行啥,cancel方法执行啥即可!其他的交给框架帮你处理!
严格一致性
执行时间短
实时性要求高
一个业务场景,也是很常见的一个异步调用场景:
支付宝往余额宝转钱
即将服务A假设为支付宝,服务B假设为余额宝。于是呢,我们的支付宝往余额宝转100块钱是怎么做的呢?特别容易,借助消息队列即可,如下图所示
一致性解决
OK,上面这一版有一个致命的问题!如下所示
事务开始
(1)给支付宝账户zhangsan,扣100元
(2)将(给余额宝账户zhangsan,加100元)封装为消息,发送给消息队列
事务结束
敢问你,如何保证第一步和第二步是在同一个事务里完成的。换句话说,第一步操作的是数据库,第二步操作的是一个消息队列,你如何保证这两步之间的一致性?
记住了,任何涉及到数据库和中间件之间的业务逻辑操作,都需要考虑二者之间的一致性。比如,你先操作了数据库,再操作缓存,数据库和缓存之间一致性如何解决?好吧,改变思路,加一张事务表,如下图所示
注意了,此时事务的内容为
事务开始
(1)给支付宝账户zhangsan,扣100元
(2)给事件表插入一条记录
事务结束
此时是对同一数据库的两张表操作,因此可以用数据库的事务进行保证。另外,起一个定时程序,定时扫描事务表,发现一个状态为’UNFINISHED’的事件,就进行封装为消息,发送到消息中间件,然后将状态改为’FINISHED’.
幂等性解决
注意了,这一版还存在一个幂等性问题!
仔细看,定时程序做了如下三个操作
(1)定时扫描事务表,发现一个状态为'UNFINISHED'的事件
(2)将事件信息,封装为消息,发送到消息中间件
(3)将事件状态改为'FINISHED'
OK,假设在步骤(2)的时候,发送完消息体,还未执行步骤(3),定时程序阵亡了!然后重启定时程序,发现刚那个事务的状态依然为’UNFINISHED’,因此重新发送。这样,就会出现重复消费问题。因此,幂等性也是需要保证的!
在消费者端,也维护一个带主键的表,可以选txid为主键,如下图所示
如果一旦出现重复消费,则在事务里直接报出主键冲突错误,从而保证了幂等性!
GTS是一款分布式事务中间件,由阿里巴巴中间件部门研发,可以为微服务架构中的分布式事务提供一站式解决方案。
GTS的核心优势:
性能超强
GTS通过大量创新,解决了事务ACID特性与高性能、高可用、低侵入不可兼得的问题。单事务分支的平均响应时间在2ms左右,3台服务器组成的集群可以支撑3万TPS以上的分布式事务请求。
应用侵入性极低
GTS对业务低侵入,业务代码最少只需要添加一行注解(@TxcTransaction)声明事务即可。业务与事务分离,将微服务从事务中解放出来,微服务关注于业务本身,不再需要考虑反向接口、幂等、回滚策略等复杂问题,极大降低了微服务开发的难度与工作量。
完整解决方案
GTS支持多种主流的服务框架,包括EDAS,Dubbo,Spring Cloud等。
有些情况下,应用需要调用第三方系统的接口,而第三方系统没有接入GTS。此时需要用到GTS的MT模式。GTS的MT模式可以等价于TCC模式,用户可以根据自身业务需求自定义每个事务阶段的具体行为。MT模式提供了更多的灵活性,可能性,以达到特殊场景下的自定义优化及特殊功能的实现。
容错能力强
GTS解决了XA事务协调器单点问题,实现真正的高可用,可以保证各种异常情况下的严格数据一致。
但是不开源!!
介绍:“LCN并不生产事务,LCN只是本地事务的协调者”
LCN分布式事务框架的核心功能是对本地事务的协调控制,框架本身并不创建事务,只是对本地事务做协调控制。因此该框架与其他第三方的框架兼容性强,支持所有的关系型数据库事务,支持多数据源,支持与第三方数据库框架一块使用(例如 sharding-jdbc),在使用框架的时候只需要添加分布式事务的注解即可,对业务的侵入性低。LCN框架主要是为微服务框架提供分布式事务的支持,在微服务框架上做了进一步的事务机制优化,在一些负载场景上LCN事务机制要比本地事务机制的性能更好,4.0以后框架开方了插件机制可以让更多的第三方框架支持进来。
特点:
①支持各种基于spring的db框架
②兼容SpringCloud、Dubbo、motan
③使用简单,低依赖,代码完全开源
④基于切面的强一致性事务框架
⑤高可用,模块可以依赖RPC模块做集群化,TxManager也可以做集群化
⑥支持本地事务和分布式事务共存
⑦支持事务补偿机制,增加事务补偿决策提醒
⑧添加插件拓展机制
选择 GTS比较N但是不开源,所以选择tx-lcn