1 本地事务
1.1 定义
本地事务,狭义上指单实例关系型数据库事务,通常也被称为传统事务,按照经典事务理论,一个事务的处理,需要具备ACID四个特性,即原子性、一致性、隔离性、持久性。
事务,是一系列对系统中数据进行访问与更新的操作所组成的一个不可分割的逻辑单元,它可以包含1笔或多笔对数据库的操作,这些操作作为一个整体,要么做完、要么不做,而且做完之后,永远承诺自己做了这件事。并且不能影响其他事务,只有事务结束后,才可以告知大家结果,尽量不让其他事务的处理,产生错觉、幻觉。
通过上面对事务的描述,原子性、隔离性、持久性这3个特性跟真实生活场景比较贴合,容易理解,而一致性相对有些抽象,查了一些资料,要么高大上,要么不太接地气,最终成了一个只可意会,不可言传的概念,下面单独死磕它。
1.2 一致性
Consistency(一致性):指事务的执行不能破坏数据库的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。也就是说,事务执行的结果必须使数据库从一个一致性的状态转变到另一个一致性状态。(摘自其他资料)
回归一致性原始概念,只有两个东西比较才会产生一致性,一个东西没有比较就不存在一致性问题,也就是说,一个事务只有1笔数据库操作就不存在一致性问题,只有多笔操作时,才会出现一致性问题。
既然是一致性,就要有一个观察的"视角",一个DB事务,如果它包含多条数据库操作,也可理解为我们把这个事委托给数据库来处理,由数据库来保证,从数据库外部看到的现象,就跟一笔操作一样,如果事务正在处理中,此时从外部观察就像事没发生一样,直到事务结束,才同时一起展示处理结果。其实从mysql数据库的内部看,就算仅1条DB操作,它也包含 buffer_pool、redo_log、bin_log 等多步操作,也存在一致性问题,但这是人家内部的事,跟外部无关,让你感知不到。
是不是似乎还有点疑惑,“多笔操作,要么一起处理,要么都不处理”,这不跟原子性一回事吗,不一样,原子性从概念上,没有保证不让看到中间态(部分处理),而数据库事务的一致性,是强一致性,数据的状态只有事务处理前的状态,和处理后的状态,没有中间态。
题外话,java的原子性操作通常认为不存在一致性问题,但如果较真,也只是由jvm来保证,从线程的视角观察,看到的是一致的,至于工作线程缓存、主存是否一致,那是jvm内部的事儿。
2 分布式事务
2.1 基本概念
分布式事务,它涉及的范围不再限于单个数据源,也不再局限于数据库层概念,但有一点是肯定的,它一定包含多个独立的子事务,子事务可以是一个关系型数据库操作,可以是一个NoSql数据库操作,也可以是第三方服务接口,也就是说,分布式事务就是一个子事务的集合,它主要是协调子事务间的处理逻辑,让它对外看上去像一个整体业务处理单元。它不关心子事务内部实现,就算子事务有问题也是它的内部问题(自己补漏、完善),跟分布式事务处理逻辑无关(只能跟着一起背锅, 不能越界)。
分布式事务只是一个技术层处理方案,对于客户(需求)来说,事务的基本原则是不变的,仍然要支持、维护事务的基本特性。
2.2 特点
本地事务
(1) 都是实时事务(短事务,也称为瞬时事务),必须在尽可能短的时间内结束。
(2) 没有子事务的概念(数据库内部的嵌套事务不算,它是内部逻辑,站在数据库边界外看,它只是一个事务)。
(3) 它的视角(事务的边界),就是数据库边界。
分布式事务
(1) 包含多个独立的子事务,且子事务不仅仅局限于数据库层。
(2) 根据实际业务,对事务完成时间要求的不同,分为短事务、长事务两种。
(3) 它的视角(事务的边界),往往以一个系统为边界,或内部服务模块为边界。
3 方案
3.1 2PC
2PC(Two Phase Commit) 两阶段提交,将事务的提交过程分为资源准备(prepare)和资源提交(commit)两个阶段,由事务协调者来协调所有资源(子事务),如果准备阶段所有资源都预留资源成功,则进行第二阶段的资源提交,否则事务协调者回滚。
特点:
(1) 通常应用在短事务场景。
(2) 如果第二阶段,事务协调者没有成功的通知到所有的资源(子事务),就会出现数据不一致。
3.2 3PC
三阶段提交,是二阶段提交协议的改进版本,主要是优化在资源不足业务场景的处理流程,处理流程分为CanCommit、PreCommmit、DoCommmit 3个阶段。
通过添加CanCommit阶段,先检查所有资源是否满足条件,如果不满足,直接取消本次事务操作, 降低了资源(子事务)回滚的概率。其次对事务通知超时等情况,也做了优化。
特点: 跟2pc一样,仍然会存在数据不一致情况,只是降低了出问题的概率。
3.3 TCC
TCC事务是"Try-Confirm-Cancel"三个单词的缩写。
TCC事务的出现, 是为解决应用拆分,带来的跨应用间事务操作的问题。
也就是,子事务是应用服务接口场景(即资源由其他服务维护),该协议对应用服务(子事务)提出规范要求,如下:
(1) 应用服务对资源的处理,必须提供以下3个接口:
Try接口:判断资源是否满足事务条件,并对资源进行冻结。
Confirm接口:对冻结资源,进行提交确认。
Cancel接口:对冻结资源,进行解冻、释放处理。
(2) 应用服务提供的3个资源处理接口,必须支持幂等性。
一旦所有应用服务(子事务)的try接口返回成功,进入确认阶段,就表明在逻辑上一定能成功(否则就是软件有bug或系统运行环境有问题),就要尽可能最大程度保证成功。
对比:
可以看出TCC也包含资源准备(prepare)和资源提交(commit)两个阶段,跟2PC的解决思路很相似,
二者主要区别是分布式事务由DB处理,还是由应用程序处理。2PC通常都是在跨库的DB层面,而TCC本质就是一个应用层面的2PC。
特点:
TCC模型对业务的侵入性强,事务回滚实际上就是自己写业务代码来进行回滚和补偿,改造的成本大。适用于核心业务场景,如支付等,通过重试、补偿等机制,来严格保证分布式事务的一致性,要么全部成功,要么全部自动回滚。
3.4 Saga
saga是分布式事务领域里一个非常重要的事务模式,特别适合解决"长事务"。
例如出行订票这种场景,持续时间长,可能持续几分钟到一两天。
1. 概念
它是将一个长事务,分拆成多个子事务,每个子事务有正向操作Ti,反向补偿操作Ci。
假如所有的子事务Ti依次成功完成,全局事务完成。
假如子事务Ti失败,那么会调用Ci, Ci-1, Ci-2 ....进行补偿。
# 回滚 & 重试
对于一个SAGA事务,如果执行过程中遭遇失败,那么接下来有两种选择,一种是进行回滚,另一种是重试继续。
2. 实现
(1) 事务状态集中管理
通常采用"状态机"的处理思路,可以从全局视角,管理整个事务。(通常推荐的方式)
(2) 事务状态分散管理
由各个子事务来负责向前或先后推动事务的进程。(适合子事务节点比较少的简单场景)
3. 特点
(1) 适用于长事务业务场景。
(2) 通常需要借助MQ,通过消息的方式来推动整个事务的进程。
4 实践(依赖第三方)
4.1 XA事务
XA是由X/Open组织提出的分布式事务的规范,它基于两阶段提交(Two-phaseCommit,2PC)协议实现,可以保证数据的强一致性。
XA 事务包含: 资源管理器(RM)、事务管理器(TM)、应用程序(AP)三种角色,其中本地数据库扮演资源(RM)角色。
第一阶段: 为准备阶段,所有的参与者RM准备执行事务并锁住需要的资源。
第二阶段: 当TM确认所有参与者都准备好后,向所有参与者发送COMMIT 命令。
XA 事务允许不同数据库间的分布式事务,只要参与全局事务中的每个结点都支持XA事务协议。目前,主流关系型数据库,例如Oracle、MySQL 和SQL Server 都支持XA事务。
特点: 性能不太理想,无法满足高并发场景。
4.2 第三方服务
目前知名度比较高的是Seata,它是阿里开源的分布式事务解决方案,支持AT、XA、TCC、Saga 四种事务模式,详情参考其官网。
4.3 事务消息
不同MQ提供的“事务消息”概念有所不同,下面只介绍RocketMQ提供的事务消息解决方案,它主要是解决消息的生产者与消息的消费者数据不一致问题。
RocketMQ事务消息基于2PC模型实现,分两阶段提交事务消息,同时增加了一个补偿逻辑来处理第二阶段超时或者失败的消息。
上面发送消息的处理流程,通过两阶段提交方式,就算处理第4步时,生产者服务宕机或网络异常,也能通过,滞后补偿的方式,保证本地数据库数据与消息两部分保持数据一致,MQ服务同样也保证消费者收到消息,基于此逻辑,解决了生产者与消费者数据不一致问题。
5 实践(自建)
5.1 本地消息表
本地消息表这个方案最初是 ebay 工程师在 2008 年发表给 ACM 的文章,设计核心是将需要分布式处理的任务通过消息的方式来异步确保执行。
补充说明:
a. 步骤1:写本地消息表和业务数据操作放在一个本地事务里,保证了业务数据和发消息的原子性,要么他们全都成功,要么全都失败,跟RocketMQ的"事务消息"很相似,只不过MQ把“本地消息表”的功能内置了。
b. 步骤5: 删除消息,只是笼统的描述,也可以修改消息状态,总之"补偿任务"可以识别,不再补发消息就可以。根据实际业务场景,可通过数据库、mq、api接口等方式来"删除消息"。
c. 随着MQ产品的可用性、功能越来越强大、可靠,它可以确保只要成功发送消息,就一定确保消费者至少消费一次,因此,在一些业务场景,步骤5已没有意义,"补偿任务"只要确保能成功发送消息就可以了。
5.2 外部消息表
跟本地消息表方案核心理念一样,只是把消息表外置,这时就没有本地事务来保证业务数据和消息数据的一致性,因此采用两步提交的方式来保证一致性(参考“事务消息”原理),该方案可降低对本地数据库性能的影响,可以把核心逻辑抽象为通用方法,降低对业务逻辑的侵入性影响。
5.3 内嵌消息表
跟本地消息表方案核心理念一样,只是把消息状态内置到业务表,不存在消息和业务数据不一致问题,它对业务逻辑的侵入性最强、影响最大,但它最容易、最简单,如果处理得当,在一些简单业务场景,它不失为一种很好的办法。
5.4 跨库(应用)
以下是处理分布式“多子事务”的简单草图,通过上面对各种分布式事务方法的了解,一般都会有事务管理器(TM)、资源(RM)、应用程序(AP) 3类角色。既然是处理事务问题,事务管理(TM)就是最核心的角色,其中"事务数据"是基础,只要能准确、清晰的描述事务状态,就可以以此拓展各种补偿、重试、回滚、通知等功能,至于是内置在业务中,还是抽象为独立的通用服务,这是工程设计、实施方面的问题,逻辑流程是不变的。
补偿说明:
a. 其中"数据通道"是泛指数据通信,可以是服务接口、MQ,或直接操作数据库,请求方式可以是同步、异步、回调等。
6 总结
分布式事务,不管是依赖第三方,还是自建处理,往往更多的不是技术问题,而是事务中业务数据、流程的定义,业务规则的理解,什么条件,允许进行补偿、重试、回滚, 什么条件必须人工处理。总之,最终目标是"可落地"的方案。