学习资源来自姜宇的《正本清源分布式事务之Seata》
以当前这种格式书写的文字,都是我学习之后的一些个人记录,可能会涉及到后面篇章的内容,导致看不懂,是正常的。我一般都是边看边记问题,后面看到能回答前面问题的地方,再回头做记录。
1.1 事务与ACID
事务是用户定义的一系列数据库操作,这些操作被视为一个完整的不可分割的工作单元,要么全部执行,要么全部不执行。
事务需要具备原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
原子性:表示要么全部成功,要么全部失败会滚。
一致性:表示系统从状态1转变到状态2,全局来看处于业务正确的状态。比如状态1时A和B各有100元,执行A给B转账50元操作,变成A50元B150元这个状态2,中间没有产生丢失或者增多就是一种一致性的保证。
隔离性:一个事务的变更在提交之前,对其他事务是不可见的。ANSI规定了4种隔离级别,严格来说只有“串行化”级别是符合这个定义的。
- 读未提交。会出现脏读。
- 读已提交。解决了脏读,但不可重复读,因为两次读可能因为其他事务的提交而导致读不一致。
- 可重复读。解决了不可重复读,但会有幻读,即范围读会因为其他事务的修改而导致多读或者少读。
- 可串行化。强制事务串行执行,不会幻读,但性能很差。
持久性:一旦事务提交了,数据应该被永久保存不会丢失。但实际上不可能百分百不丢失数据,只能根据成本做取舍。
1.2 分布式CAP理论和BASE理论
CAP理论
CAP是分布式系统的指导理论,它指出一个分布式系统不可能同时满足一致性(Consistency),可用性(Availability)和分区容错性(Partition Tolerance),最多只能同时满足其中两项。
一致性:all nodes see the same data at the same time,即操作完成后,所有节点在同一时间的数据处于全局性的正确状态。
可用性:即服务一直可用。
分区容错性:即分布式系统遇到某节点或网络分区故障时,仍然恩能够对外提供满足一致性和可用性的服务。分区容错性是最基本的要求,否则搞分布式就没有意义了。
系统架构师往往需要根据业务特点选择CP还是AP:
- CP,即实现一致性和分区容错性,要求多服务之间数据的强一致性,弱化可用性,对数据要求比较高如金融业务会使用这个模式,但性能偏低,常用XA两阶段提交或者SeataAT模式的“读已提交”级别,后面会介绍这两种分布式事务方案。
- AP,即实现可用性和分区容错性,通常只要求最终一致性,强调所有服务可用,允许中间不一致,最终达到一致性即可。互联网分布式服务多基于AP,性能高,满足高并发的业务需求。常用的分布式事务方案有TCC、基于消息的最终一致性、Saga等。
BASE理论
Base是Basically Available(基本可用)、Soft State(软状态)和Eventually Consistency(最终一致性)的缩写。
BASE理论是对CAP中一致性及可用性进行权衡的结果,其核心思想是:无法做到强一致性,那么可以通过牺牲强一致性来获得可用性。
基本可用:基本可用是对可用性的一个妥协,即在分布式系统出现不可预知的故障时,允许损失部分可用性。(比如秒杀场景和雪崩场景下的降级处理)
软状态:软状态描述的是原子性,要求多个节点的数据一致是一种“硬状态”,软状态指的是允许系统的数据存在中间状态,并认为该状态不影响系统的可用性,即允许不同的节点间存在数据延时。
最终一致性:不可能一直处于“软状态”,必须有个时间期限,期限过后,应当保证节点间数据一致性,从而达到数据的最终一致性。
总体来说,BASE理论面向的是大型高可用,可扩展的分布式系统,通过牺牲强一致性来获得可用性,允许一定时间内的不一致,但最终达到一致。
1.3 分布式事务
XA两阶段提交协议
XA两阶段提交协议由Tuxedo提出给X/Open组织,作为资源管理器与事务协调器的接口标准。
个人理解,在分布式系统中,资源管理器应该是各个数据库实例,事务协调器通常是额外引入支持XA的中间件,比如Cobar,Mycat,ShardingSphere,当然也包含Seata。
而在mysql内部的事务提交其实也是用了XA两阶段提交,innodb引擎作为资源管理器负责prepare_commit,server层binlog作为事务协调器以binlog写入并刷盘标志事务提交成功,接着通知innodb改为commit状态。
XA协议包括两套函数--以xa_开头的函数及以ax_开头的函数:
-
xa_open()、xa_close():建立/关闭与资源管理器的连接。
-
xa_start()、xa_end():开始/结束一个本地事务。
-
xa_prepare()、xa_commit()、xa_rollback():预提交/提交/回滚一个本地事务。
-
xa_recover():回滚一个以进行预提交的事务。
-
ax_reg()、ax_unreg():允许一个资源管理器在事务协调器中动态注册/撤销注册。
XA两阶段提交协议被用来管理分布式事务,可以保证数据的强一致性,能够解决很多临时性系统故障(包括进程、网络节点、通信等故障),许多关系型数据库都提供了对XA的支持,如Oracle, DB2, Mysql等。
两阶段提交协议为了保证事务的一致性,不管是事务协调器还是资源管理器的每一步操作都会记录日志。日志降低了性能,但提高了系统故障恢复能力。
两阶段提交的过程
一阶段,应用程序向事务协调器发起提交请求,此后分为两个步骤:
(1) 事务协调器通知参与该事务的所有资源管理器开始准备事务。
(2) 事务管理器在接受到消息后开始准备(写好事务日志并执行事务,但不提交),之后将“就绪”的消息返回给事务协调器。
二阶段,也分两个步骤:
(1) 事务协调器在接收到各个资源管理器回复后,基于投票结果进行决策--提交或取消。如果有任意一个回复失败,则发送回滚命令,否则发送提交命令。
(2) 各个资源管理器在接收到二阶段提交或回滚命令后,执行并将结果返回给事务协调器。
两阶段提交的缺点
(1) 同步阻塞问题。在执行过程中,所有参与节点都是事务阻塞型的--当参与者占用公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
学习了Seata AT模式之后,反思对这个问题的优化,SeataAT的优化在于每个分支事务都是执行完事务反馈给事务协调器执行成功之后立即本地提交,释放了本地连接资源和锁资源,只有事务协调器保留这全局事务锁,只会导致同样需要操作重叠数据的其他全局事务的阻塞,不影响非全局事务的操作和任何读操作,当然如果开启读提交的模式也会影响相应数据的读操作。
(2) 单点故障。事务协调器起着关键作用,一旦故障,参与者会一直阻塞下去,尤其是第二阶段,所有参与者还处在锁定事务资源的状态,无法继续完成事务。
这点我个人觉得还好,正常实现XA的中间应该都支持多实例高可用,包括Seata本身也是如此。
(3) 数据不一致。在二阶段,如果事务协调器向参与者发送commit过程中请求局部网络异常,或者事务协调器故障,会导致只有一部分参与者接收到commit请求;导致一部分参与者执行了commit,另一部分没执行,于是出现数据不一致的现象。
这点我同样认为主要看中间件的实现,一般都会记录完整的日志,以便恢复,Seata同样如此,不能完全避免数据不一致的问题。
(4) 状态不确定。如果事务协调器发出commit后宕机了,唯一收到这个消息的参与者也宕机了,那么即使通过选举协议产生了新的事务协调器,事务的状态也是不确定的,因为没人知道事务是否已经被提交了。
同样的,我认为有日志就能恢复,日志知道是否应该被提交以及是否已经提交。
我认为这里需要加上第(5)点,两阶段提交容易出现死锁问题,尤其是如果两个事务分别先后锁住数据库1和数据库2的数据进入互相等待,就算开启了各自数据库的循环等待的死锁检测也检测不出来,因为循环发生在全局,在单个数据库中并没有形成循环等待。(不过如果开启锁等待超时快速失败,应该跟下面seata的方案效果类似)
seata通过以下三点避免死锁(
(1) 分支事务先获取数据库锁再获取全局锁,这个顺序是固定的。
(2) 在获取全局锁之前,不会释放数据库锁。
(3) 获取不到全局锁不会一直等,而是快速失败并快速释放数据库锁。
TCC柔性事务
这么快就TCC了,我记得还有一个三阶段提交的方案,可能是因为大部分数据库支持的XA都是二阶段没有支持三阶段的原因所以没有讲吧。
TCC(Try-Confirm-Cancel)的核心思想是:通过对资源的预留(如冻结金额),尽早释放对资源的加锁;如果事务可以提交,则完成对预留资源的确认;如果要回滚,则释放预留的资源。
这里说的尽早释放对资源的加锁,举个例子更好理解,比如转账,需要第一步扣掉我账户的金额,第二步增加他账户的金额,很显然对我账户的金额加了锁,而且要等待整个事务完成提交之后才能释放,如果通过预留的方式把要扣掉的金额冻结,这样就可以很快释放对我账户金额的锁了,其他事务不需要等待这个事务即可立即操作。
TCC方案在电商,金融领域落地较多。TCC其实是两阶段提交的一种改进,但是对业务侵入大,资源锁定交由业务方完成,工作量也相当大。
TCC方案将整个业务逻辑的每个分支显式地分成try、confirm、cancel3个阶段:try完成业务的准备工作,confirm完成业务的提交,cancel完成业务的回滚。
try、confirm、cancel、操作可以与XA的prepare、commit、rollback接口类比,区别在于:
- 前者在开发者层面式能感知的,这3个阶段都由开发者自己去实现。
- 后者在开发者层面是不感知的,数据库自动完成资源的操作。
TCC流程:
(1) 业务应用向事务协调器发起开始事务请求。
(2) 业务应用调用所有服务的try接口,完成一阶段工作。
(3) 业务应用根据调用try接口是否成功,决定提交或回滚事务,并发送请求到事务协调器。
(4) 事务协调器根据接收到的请求,决定调用confirm接口或cancel接口。如果接口调用失败,则会重试。
TCC的优点:
应用自己定义数据库操作的粒度,减少了锁冲突,提高了吞吐量。
TCC的缺点
(1) 对应用的侵入性强。
(2) 实现难度大。需要根据网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等性。
基于消息的最终一致性
从本质来说,消息方案是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。
在普通消息处理过程中,存在数据库数据与消息不一致的问题,进而造成消息生产者与消息消费者数据不一致。
流程:
(1) 消息生产者在完成本地业务操作(通常是一个数据库本地事务)后,发送消息到MQ。
(2) MQ收到消息,将消息持久化,在存储系统中新增一条记录。
(3) MQ返回ACK给消息生产者。
(4) MQ推送消息给对应的消息消费者,然后等待消息消费者返回ACK。
(5) 消息消费者在收到消息后完成本地业务操作(通常为一个数据库本地事务),返回ACK。
(6) MQ删除消息。
问题分析:
生产者的消息发送与本地事务没办法确保原子性,即不管先发消息后提交还是先提交后发消息,都有肯能一个成功一个失败,本身消息队列与数据是两个独立的数据源也没法确保一起成功或一起回滚,因此会生产者与消费者数据不一致。
消费者反馈消息已消费与本地事务提交也同样存在类似问题。
解决方案:
在一些对数据一致性要求很高的场景中,经常采用基于消息的最终一致性方案,通过消息中间件来保证上下游应用数据的一致性。
流程如下:
(1) 在执行业务操作时,记录一条消息数据到数据库(状态为"待发送"),并且消息数据的记录与业务数据的记录必须在数据库的同一个本地事务内完成(这是该方案的核心保障)。
(2) 在消息数据记录完成后,通过一个定时任务轮训状态为“待发送”的消息,然后将待发送的消息投递给消息队列。
(3) 如果在这个过程中投递失败,则启动重试机制,直到成为收到消息队列ACK后,再将消息状态更新为“已发送”或者删除消息。
(4) 如果下游系统消费失败,则不断进行幂等重试,最终做到两个系统数据的最终一致性。
缺点依然是对应用侵入性很强,改造成本较高。
网友评论