前言
什么是事务
事务(Transaction)是指程序执行过程中的一个不可分割的逻辑单位,由一个有限的操作序列构成。
什么是分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
事务隔离性
-
脏读
:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。 -
幻读
:在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A新增提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。 -
不可重复读
:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A更新或删除提交前读到的结果,和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这会导致锁竞争加剧,影响性能。
事务的隔离级别
-
Read Uncommitted
(读未提交):最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。 -
Read Committed
(读已提交):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。 -
Repeated Read
(可重复读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。 -
Serialization
:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。
数据库中事务的四大特性
数据库事务要满足ACID的特性:
- Atomic(原子性):一个事务是一个不可分割的工作单位,事务中的所有操作,要么全部成功,要么全部失败回滚。
- Consistent(一致性): 事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果数据库的状态满足所有的完整性约束,就说该数据库是一致的。
- Isolation(隔离性):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据,对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- Duration(持久性): 一个事务一旦被提交,它对数据库中数据的改变就是永久性的。接下来的其他操作或故障不应该对其有任何影响。持久性通常还需要数据库备份和恢复来保证。
分布式事务的意义
从 CAP 定理来看,P(可分区容错性)一般来说是分布式系统无法规避的既定事实,所以
我们更需要在C(强一致性)A(可用性)方面做权衡与取舍。
对于分布式系统来说,我们当然追求高可用性,但是有时候我们也需要在发生异常情况下依然有一致性保障(如机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失等异常情况)。
一致性通常有以下几种分类:
-
强一致性
:读操作可以立即读到提交的更新操作。 -
弱一致性
:提交的更新操作,不一定立即会被读操作读到,读操作读到最新值需要一段时间。 -
最终一致性
:是弱一致性的特例。事务A更新一份数据后,最终一致性保证在没有其他事务更新同样的值的话,最终所有的事务都会读到事务A更新的最新值。如果没有错误发生,不一致时间的长短依赖于:通信延迟,系统负载等。
分布式事务的演变
单服务单数据库的本地事务
事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务
单一服务多数据库的分布式事务
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
多服务多数据库的分布式事务
当一个服务操作访问不同的数据库资源,又希望对它们的访问具有事务特性时,就需要采用分布式事务来协调所有的事务参与者。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务
多服务多数据源的分布式事务
如果将上面这两种场景(一个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成一种树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
分布式事务解决方案
基于XA协议的两阶段提交(2PC)
X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型;
XA协议
:XA 是 X/Open定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等, XA 接口函数由数据库厂商提供。
两阶段提交协议分为两个阶段进行,第一阶段是预处理阶段,也就是发送 SQL 语句,业务系统校验数据库返回的响应进行校验。第二阶段是提交阶段,也就是发送 commit 指令给数据库。
-
第一阶段 预处理阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败 ,要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交。其预处理要分为三个子步骤:
-
协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
-
参与者节点执行询问发起为止的所有事务操作,并将 Undo 信息和 Redo 信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
-
第二阶段 提交阶段:如果协调者收到了参与者的失败或超时,直接给每个参与者发送回滚(rollback)操作;否则发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。
无论最后结果如何,第二阶段都结束当前事务。
两阶段有如下几个缺点:
- 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。在第一阶段,做预处理时会锁住对应的资源,到第二阶段收到协调者发送的 commit 或者 rollback 消息才释放锁资源,则协调者发送回滚操作才释放锁资源。在锁资源到释放资源过程,会阻塞第三方节点访问该锁资源。
- 单点故障。由于协调者的重要性,一旦协调者发生故障,则无法继续完成事务操作。
- 数据不一致。在第二阶段中,当协调者向发送 commit 消息请求之后,发生了局部网络异常或者发送 commit 请求过程中协调者发生了故障,会出现部分参与者接收到 commit 消息并进行提交,而其他参与者未收到 commit 消息则无法提交,于是整个事务中就会出现数据不一致的现象。
- 二阶段无法解决的问题。协调者再发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
其在应用工程中很少使用,但其有一定的参考性;
三段提交(3PC)
三段提交是两段提交的升级版,主要的改动为:
- 引入了超时机制,同时在协调者和参与者都引入超时机制
- 在第一阶段和第二阶段中插入准备阶段。保证了在最后提交阶段之前各参与者的状态是一致的。
三阶段提交提交协议,分为三个阶段,CanCommit 阶段、PreCommit 阶段、DoCommit 阶段
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
会导致数据一致性问题。由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。
TCC补偿机制
TTCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。三个阶段如下:
操作方法 | 含义 |
---|---|
Try | 预留业务资源/数据效验-尝试检查当前操作是否可执行 |
Confirm | 确认执行业务操作,实际提交数据,不做任何业务检查。try成功,confirm必定成功 |
Cancel | 执行业务出错时,需要回滚数据的状态下执行的业务逻辑 |
其核心在于将业务分为两个操作步骤完成。不依赖事务协调器对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
- 为了避免重复发送消息,所有的子业务系统都必须提供接口幂等性原则。
- 为了避免主业务系统宕机,无法知晓这次事务的执行情况,需提供一个机制,记录这次事务的执行状态,待服务器重新提供服务或者定时任务定时处理事务后续的动作,以确保事务的完整性。
最大努力通知
通过异步的形式,或者采用第三方组件将请求确保将消息发送给子业务系统。其需要评估该请求在子业务必须执行成功的;否则子业务系统执行失败,主业务系统还需要进行回滚,逻辑将会变得复杂;什么情况下,会采用该机制呢?主要是发短信等必须执行成功的场景下,才会采用该方法
本地消息表
请求保存到本地数据库,交由后台程序异步将请求推送给子业务系统。一般来讲,设计重试 3 次即可,当连续发送三次失败,即通知运维人员进行跟进处理,具体什么原因导致;如果涉及不限次数的发送,那么就会导致业务系统压力过大
工作流程:
- 消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
- 消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
- 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
优点:
一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点:
消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
MQ 事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如
RabbitMQ 和 Kafka 都不支持(RabbitMQ、Kafka基于ACK机制)。
以阿里的 RocketMQ 中间件为例,流程为:
- 发送一个事务消息,这个时候,RocketMQ将消息状态标记为Prepared,注意此时这条消息消费者是无法消费到的。
- 执行业务代码逻辑。
- 确认发送消息,RocketMQ将消息状态标记为可消费,这个时候消费者才能真正消费到这条消息。
- 如果步骤3确认消息发送失败,RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认。RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
Seata
2019 年 1 月,阿里巴巴中间件团队发起了开源项目
Fescar
(Fast & EaSy Commit And Rollback)
,和社区一起共建开源分布式事务解决方案。Fescar
的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。
Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。
为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为
Seata
,意为:
Simple Extensible Autonomous Transaction Architecture
,是一套一站式分布式事务解决方案。
Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验。