分布式事务理解
1.事务的概念
事务,其实是包含一系列操作的、一个有边界的工作序列,有明确的开始和结束标志,且要么被完全执行, 要么完全失败,即all or nothing。通常情况下,我们所说的事务指的都是本地事务,也就是在单机上的事 务。
2.本地事务
单机事务.png
本地事务四大特性(ACID):
原子性(atomicity):一个事务中的所有操作,不可分割,要么全部成功,要么全部失败;
一致性(consistency):一个事务执行前与执行后数据的完整性必须保持一致;
隔离性(isolation):一个事务的执行,不能被其他事务干扰,多并发时事务之间要相互隔离;
持久性(durability):一个事务一旦被提交,它对数据库中数据的改变是永久性的。
3.分布式事务
分布式事务.png
分布式事务的实现主要有以下 5 种方案:
XA方案
事务补偿型方案TCC
本地消息表
可靠消息最终一致性方案
最大努力通知行方案
xa方案
1.XA规范
X/Open组织(现在的Open Group)定义了一套DTP(Distributed Transaction Processing)分布式事务处理模型,主要包含以下四部分:
AP(应用程序)
TM(事务管理器):交易中间件
RM(资源管理器):数据库
CRM(通信资源管理器):消息中间件
XA规范则是DTP模型定义TM和RM之间通讯的接口规范。XA接口函数由数据库厂商提供。TM用它来通知数据库事务的开始、结束、提交、回滚。基于XA规范衍生出下面的二阶段提交(2PC)、三阶段提交(3PC)。
XA规范包括两套函数,以xa_开头的及以ax_开头的。
以下的函数使事务管理器可以对资源管理器进行的操作:
xa_open,xa_close:建立和关闭与资源管理器的连接。
xa_start,xa_end:开始和结束一个本地事务。
xa_prepare,xa_commit,xa_rollback:预提交、提交、回滚一个本地事务。
xa_recover:回滚一个已进行预提交的事务。
ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。
XA的一些问题:
性能(阻塞、响应时间增加、死锁);
依赖于独立的J2EE中间件,Weblogic、Jboss,后期轻量级的Atomikos、Narayana、Bitronix;
不是所有资源(RM)都支持XA协议;
2.JTA(Java Transaction API)
即Java的事务API,基于XA实现,也就是RM需要支持XA,所以也有JTA(XA)的说法,JTA仅定义了接口。主要包括javax.sql.XADataResource、javax.sql.XAConnection、javax.sql.XAException、javax.transaction.xa.XAResource、javax.transaction.Xid。
目下JTA的实现有几种形式:
J2EE容器提供的JTA实现(Weblogic、JBoss );
JOTM(Java Open Transaction Manager)、Atomikos,可独立于J2EE容器的环境下实现JTA;
3.二阶段提交2PC
2PC就是分布式事务中将事务分为两步进行提交。基于数据库的XA协议完成事务本质上就是二阶段提交(XA、JTA/JTS)。
协调者(Coordinater):事务管理器(TM)
参与者(participants):资源管理器(RM)
准备阶段:
协调者向参与者发送prepare信息,以询问参与者是否能够提交事务;
参与者在收到prepare信息后,进行本地事务的预处理,但不提交。并根据处理结果返回,失败not commit or 成功ready ;
提交阶段:
如协调者收到参与者的失败消息,则向每个参与者发送rollback消息进行回滚;
所有参与者都返回ready,则向每个参与者发送提交commit消息,通知参与者进行事务提交;
二阶段提交的一些问题:
同步阻塞,事务执行过程中所有参与者都是阻塞型的,第三方参与者访问参与者占有的资源时会被阻塞;
单点故障,协调者一旦发生故障,参与者会被阻塞。尤其在提交阶段,所有参与者都处于锁定资源状态中,无法完成事务操作;(可以选择新的协调者,但无法解决参与者被阻塞的问题);
数据不一致,提交阶段协调者向参与者发送commit信息,发生局部网络故障,会导致存在参与者未收到commit信息无法提交事务情况,导致出现数据不一致现象;
实践demo:https://github.com/cderlearner/xa-demo
4.三阶段提交3PC
相比于2PC,3PC把2PC的准备阶段再次进行拆分,并且3PC引入了参与者超时机制。
canCommit:协调者询问参与者,是否具备执行事务的条件,参与者进行自身事务必要条件的检查;
preCommit:协调者通知参与者进行事务的预提交;
doCommit:协调者根据preCommit阶段参与者的反馈结果通知参与者是否进行事务提交或是进行事务回滚;
事务补偿型方案tcc
TCC的核心思想就是校验、资源锁定、补偿,对每个操作(Try)都提供确认(Confirm)和取消(cancel)的操作,这样根据操作的结果,来确认是进行Confirm还是Cancel。
可以看出XA的两阶段提交是基于资源层面的,而TCC也是一种两阶段提交,但它是基于应用层面的。
Try:主要负责对业务进行数据检查和资源预留,例如:对资金进行冻结;对状态更改为处理中;
Confirm:确认执行业务的操作,例如:进行实际资金扣除;更改状态为最终结果;
Cancel:取消执行业务的操作,例如:解冻资金;更改状态为未处理;
TCC存在的一些问题:
业务操作的是不同服务的Try来进行资源预留,每个Try都是独立完成本地事务,因此不会对资源一直加锁。
业务服务需要提供try、confirm、cancel,业务侵入性强,如不适用三方框架要做到对各阶段状态的感知,比较麻烦。
常用TCC框架:tcc-transaction、ByteTCC、spring-cloud-rest-tcc、Himly
Confirm/Cancel要做幂等性设计。
常见的微服务系统大部分接口调用是同步的,这时候使用TCC来保证一致性是比较合适的。
实践demo:TODO
本地消息表
本地消息表其实是国外的 ebay 搞出来的这么一套思想。
这个大概意思是这样的:
A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
接着 A 系统将这个消息发送到 MQ 中去;
B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息;
B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。
存在的问题:
这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,如果是高并发场景咋办呢?咋扩展呢?所以一般很少用。
可靠消息最终一致性方案
这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。
大概的意思就是:
- A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
- 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
- mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
- 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你就用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ或者RabbitMQ自己封装一套类似的逻辑出来,大致架构思路如下图:
分布式事务真复杂.png
其中消息状态维护三种状态:
- init
- sent
- end
最大努力通知行方案
这个方案的大致意思就是:
1.系统 A 本地事务执行完之后,发送个消息到 MQ;
2.这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
3.要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。