1、分布式事务
事务的本质是保证数据一致性
分布式事务是微服务架构中必须要解决的问题,但大家又说事务会影响性能,同时分布式事务又是很复杂的东西,使得大家都很迷茫什么时候才应该使用事务,也并不知道什么场景下才使用分布式事务。
接下来我们就使用一些例子,数据库事务、分布式事务等工作原理来了解相关的应用场景。
那么,什么是分布式事务呢?
说白了,事务就是把多次数据操作,打包成一个整体,要么这些操作都成功,要么都失败。
从大角度来说,事务就是保证数据的一致性,在一个事务中,我们可能需要同时保证mysql的数据一致性,也有可能是nosql类似redis的一致性,甚至是service中存储的有状态的内存数据,更甚者,需要同时保证mysql、redis及mongodb的数据一致性。
从小角度来说,数据库事务,指单个数据库的事务,分布式事务指多个数据库的事务。
1.1、数据库事务
我们以mysql数据库为例,mysql的innodb引擎本身支持事务,他的工作原理是这样的:
- 请求端发送“开启事务的SQL语句(begin)”到数据库以开启事务。
- 事务开启期间,请求端可发送多条SQL语句以操作数据。数据库会根据事务等级及相关的数据表或数据行阻塞其他数据操作(直到事务结束)。
- 操作完毕后,请求端可根据数据操作是否成功以及自身业务要求。决定发送“提交或回滚事务(commit/rollback)”的SQL语句,以告诉数据库是否将之前操作成功的数据进行回滚。
数据库事务影响数据库性能的原因有两个:
- 事务操作期间,可能会阻塞其他数据操作,具体需要根据事务等级、相关数据表或数据行而定
- 若事务需要回滚,则一般会锁表,阻塞所有操作相关表的数据操作
在实际开发中,数据库事务开启是非常简单的。以spring框架为例,在Service函数中添加事务注解即可。框架会根据数据库操作是否出现exception(异常,说明该次数据操作失败)而决定回滚。
@Service
public class MyService{
//开启事务注解
@Transational
public String serviceFunction(){
xxDao.update();
xxsDao.insert();
}
}
1.2、分布式事务
对于分布式事务而言,他是为了解决多个数据库一致性问题。但对于分布式事务的实现方式是很复杂的。
1.2.1、过度设计
在聊分布式事务解决方案之前,我们必须的,不得不提的一个问题,就是过度设计。通常来讲过度设计分为两种
- 多条数据设计在一个数据库,使用本地事务就好了,而不需要上分布式事务
- 业务流程究竟需不需要使用事务回滚
例如,账户余额表和优惠劵表,他们关联性很强,都属于用户数据的范畴,完全没有必要分离出这两个系统,合并一个就可以了。这也是市面上大多项目的现状,系统架构不完善,出现了很多大可不必的分布式事务问题。
又比如,某个活动上架了一批ps5库存,并推送给所有提前订阅消息的用户。对于这种情况而言,推送消息失败是不应该做分布式事务的,因为如果推送消息失败,库存应该仍然更新,而不应该回滚。
其实这也说明了一个问题,数据库事务与分布式事务的区别。除了单个数据库与多个数据库的区别,还有一个更加重要的区别:业务上的区别。
一个数据库内的表是业务关联性较强的,所以数据库事务是频繁的。但是多个数据库间由于所属的业务子系统是不一样的,所以业务关联性是不强的(数据不一致的要求不高)。自然需要分布式事务的场景其实也不多(过度设计除外)。
所以,大多数场景下,需要同时操作多个数据库的话。一般前端整合多个子系统的API即可,或者由一个后端程序调用其他系统的API。
当然,分布式事务场景肯定也是存在的,比如订单和库存系统。
下面我们会介绍一些常用的分布式事务实现,分布式事务的理论做法有很多(2PC、3PC、TCC、本地消息表)。我们大致介绍几种。
1.2.2、分布式事务解决方案
1.2.2.1、XA事务
XA方案,XA事务是数据库原生支持的分布式事务(MYSQL、Oracle等)
XA事务的工作原理依赖于数据库原生的事务,我们可以简单理解为同时执行多个数据库的数据库事务,XA事务也有相关的操作框架,比如JTA、Seata等等。
XA事务在实现上是相对简单的(不需要使用其他中间件辅助),但在大型网站系统中,这种方式是不被提倡的。因为它同时阻塞了多个数据库(同时浪费多个数据库性能)。
同时,XA事务也意味着一个后端程序需要同时操作多个数据库。除非是这多个数据库。存储的是相同类型的数据(数据分片存储),如由于用户信息太多而存储在多个数据库中,不然XA事务的应用场景其实不多。
1.2.2.2、Seata框架
接下来是Seata分布式事务框架,Seata的大致工作原理如图所示(工作模式实际上有4种,不同模式有所区别)。Seata需要额外部署事务协调服务(TC)作为全局监控的中间件,后端程序需要嵌入Seata的事务管理器(TM)、资源管理器(RM)等代码。各个后端程序向事务协调服务报告其执行的结果,并根据全局事务的结果决定是否回滚已经操作成功的数据。
对于这种技术团队理解上存在门槛的技术,我们一定要慎用。
下面我们依次介绍Seata的四种模式。
1.AT模式(Seata默认)
AT模式是阿里Seata独有的模式,通过生成反向SQL实现数据回滚,需要在数据库额外附加UNDO_LOG表。
实现原理
AT的工作原理:我们需要在数据库中添加一个名为 UNDO_LOG
的表,这个表用于存储回滚日志。在执行阶段,会执行相应的语句并提交,同时反向解析SQL语句,向 UNDO_LOG
表中写入反向解析的SQL,如果事务提交,则删除 UNDO_LOG
表中的数据,如果事务回滚,那么就根据执行 UNDO_LOG
表中的回滚SQL。
例如:我现在有订单和仓储两个数据库
第一阶段
订单表
insert into 订单表 values(10001, ... )
仓储表
update 仓储表 set num = 200 where gid = 100 //库存原来210
自动生成 UNDO_LOG
回滚日志,插入到该表中
delete from 订单表 where id = 10001
仓储表
update 仓储表 set num = 210 where gid = 100
第二阶段
如果执行提交操作,则删除 UNDO_LOG
表中的记录
如果执行回滚操作,则执行 UNDO_LOG
表中的反向SQL语句。值得提一嘴的是,Seata事实上还记录了操作之前的数据,在执行反向SQL的时候还会对比一下当前数据库数据是否与之前相同(其实是乐观锁原理),如果不同,相当于当前数据被其他外部操作过了,这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
特点
性能:一般
模式:AP,存在数据不一致的中间状态。(语句对于数据库而言已经提交了,但对于事务而言未完成,属于读未提交的数据)
难易程度:简单,依赖Seata自己反向解析SQL并回滚。
使用要求:
- 所有服务与数据库必须要自己拥有管理权,因为要创建
UNDO_LOG
表 - 支持本地ACID事务的关系型数据库,java应用并通过JDBC访问数据库
应用场景:
- 接入简单,不需要我们手写反向SQL,需要多插入表数据,同时也会持有记录锁,因此效率相对不高
隔离级别
写隔离:
一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到 全局锁 ,不能提交本地事务。拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。这里的 全局锁 是当前操作记录的 全局锁 。
读隔离:
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
2.XA模式
对于XA模式而言,其完全依赖于DB本身支持的事务,可以聊的确实不多。
实现原理
事务管理器发送开启事务消息,事务协调器接收消息交由资源管理器操作db,发送开启事务命令,多个事务协调器上报信息给事务管理器
提交:如果都没出现异常,事务管理器发送提交消息,资源管理器操作db提交
回滚:如果有一个事务协调器发送异常指令,事务管理器则向各个事务协调器发送回滚指令,资源管理器操作db回滚
特点
性能:一般
模式:ap
难易程度:简单。
使用要求:db原生支持事务。
3.TCC模式
TCC模式被Seata所兼容。所谓TCC模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
实现原理
TCC模式实际上仍然是2PC,但不同于AT模式自动解析反向SQL,自动执行提交或者回滚操作,TCC的提交和补偿操作均需要自定义。
- 一阶段 prepare(准备) 行为
- 二阶段 commit(提交) 或 rollback(回滚) 行为
特点
性能:高
模式:ap
难易程度:简单,只需要自定义三个方法即可,准备、提交、回滚
优缺点:
- 与db解耦,甚至可以满足大角度,类似mysql和内存数据的一致性事务
- 性能很高
- 与代码耦合性强
性能高
TCC模式的性能其实是很高的,我们将他与AT模式进行对比会发现。AT会自动解析反向SQL,会自动完成对回滚或者提交的操作,而TCC模式三个操作都需要我们自定义。但其性能上是很高的,我们知道,AT模式下要保证一致性,必须持有行锁,而TCC因为自定义三个事务方法的原因,在某些场景下不需要持有行锁。那么我们将会使用一个示例展示说明。
我们依然以订单和仓储表为例
第一阶段
第一阶段,准备阶段,实际上两个模式SQL是相同的。
订单表
insert into 订单表 values(10001, ... )
仓储表
update 仓储表 set num = 200 where gid = 100 //库存原来210
第二阶段
对于AT模式而言
订单表
自动生成 UNDO_LOG
回滚日志,插入到该表中
delete from 订单表 where id = 10001
仓储表
update 仓储表 set num = 210 where gid = 100
关键点在于反向解析的SQL是回滚原来的数据,初始数据210扣减10个库存,采用的是set num = 210
,在事务执行期间,是不允许有其他线程或者业务读取到200这个数据的,如果允许则出现脏读现象,这样回滚的时候势必照成丢失修改。
如果我们使用的是TCC模式,可以自定义SQL语句
订单表手动sql如下
delete from 订单表 where id = 10001
仓储表手动sql如下
update 仓储表 set num = num + 10 where gid = 100
不同于AT模式反向解析SQL是回滚原始数据,我们采取的方式是增加10个库存,因此在事务期间其他事务是可以持有库存表锁的,也就是他是一个共享锁的关系,并不会阻塞。
总结:TCC实现的分布式事务性能是非常高的,他不仅体现在了三个阶段可自定义的灵活性,同时,也不需要类似于AT那样依赖Seata反向解析SQL,毕竟反向解析和存储SQL都需要额外的性能。
4.Sage模式
Sage模式在Seata中被描述成了一种长事务结局方案,实际上我是并不认同的,其实他更像是一种流程中心的解决方案。在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
参考官方示例图
可见,Sage模式实际上是另外一种解决方案了,也就是失败执行补偿方法。但Sage真的就像示例图这么简单吗?
实现原理
Sage的实现是基于状态机引擎实现的,官方机制如下:
- 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
- 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
- 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
注意: 异常发生时是否进行补偿也可由用户自定义决定
- 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
可见,Sage更像是一种流程中心的解决方案,而Seata就是流程中心服务,他规定了业务流程的执行过程,当出现异常的时候,是否重试、补偿和回滚,是否执行其他方法和接口,均由我们自定义流程。
下面我们使用Seata官方示例来了解一下Sage流程中心的魅力。Seata官方提供了一个状态机设计器,以供给我们设计业务
当前设计的一整个流程被称之为状态机,左侧为状态机构件,每个构件可以在右侧设计基本属性。如图所示,我们在选择线条下属性中的 Expression
表示判断的内容,当 a==2
的时候执行这条线的子状态机。同时线条属性 Default
表示当前线条是否是选择标签默认执行的路线。
我们再来看看类似的ServiceTask构件(服务)的属性。
{
"ServiceName": "", // 服务名
"ServiceMethod": "", //调用的服务名称,如果使用dubbo,就可以便捷的去调用RPC方法
"Input": [ // 调用服务的输入参数列表, 是一个数组, 对应于服务方法的参数列表
{}
],
"Output": {}, //将服务返回的参数赋值到状态机上下文中, 是一个map结构
"Status": {}, //服务执行状态映射,框架定义了三个状态,SU 成功、FA 失败、UN 未知
"Retry": [] //捕获异常后的重试策略, 是个数组可以配置多个规则
}
更加详细的状态机配置与构件的属性信息请阅读官方文档。
可见,在流程中心中,我们可以自定义业务的流程了,不再拘泥于出现异常就执行回滚。
特点
性能:很高
模式:最终一致性,所以性能很高
难易程度:很难,需要学习状态机以及dubbo。
应用场景:
- 复杂业务
- 简单的回滚不满足业务需求,需要执行重试或者补偿机制
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性(应对方案见后面文档)
1.2.2.3、持久化事务消息
持久化事务消息是保证最终一致性的极限托底方案,他在事务开始时就持久化事务元数据,因此能够保证无论如何都不会出现任何的数据不一致的问题。并且由于其保证的是一个最终一致性,性能并不差,因此在数据安全非常重要的场景中很常见。
实现原理也非常简单,该方案中主要有两种角色:事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息,事务被动方接收并执行事务发起方发送的处理机制。
由于本地持久化事务的流水表,因此就是真的出现不一致问题,也存在事务记录,人工工单查询并处理。
1、本地消息表
本地消息表会依赖于事务主动方的db,需要事务主动方创建本地事务消息流水表来记录事务流水。依赖于本地事务,将业务和事务流水表耦合成一个事务,保证一个分布式事务的发起。接着,我们可以采用消息中间件MQ或者直接远程调用的方式,调用事务被动放执行其业务。如果业务执行失败,通常我们会不断的重试去保证业务的消费。
当事务主动方出现异常,那么本地事务直接回滚即可。当消息被动方出现异常,根据本地消息流水表重试就好了,如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务。
2、MQ
上面本地消息表的实现在流程上是没有问题的。但会存在一些开发中的耦合,不便于维护。因此我们使用MQ将消息发送,重试和回滚等对消息的耦合相关接口解耦开来。
实现原理
以RocketMQ为例
- 生产者(事务主动方)发送事务消息会先发送一个 半消息 ,这个半消息消费者(事务被动方)是无法接收的,并且半消息会进行持久化在MQ中。
- 接着生产者(事务主动方)才会执行对应的业务流程,并对MQ发送消息确认信息,此时 半消息 经过确认之后才会被消费者(事务被动方)消费。
- 只有当消费者(事务被动方)消费成功返回消息确认之后,该事务才算正常完成。
对于持久化事务消息而言,常用MQ方案来实现分布式事务问题。
特点
性能:低(依然需要持有行锁,并且添加了MQ中间件,RT时间长)
模式:强一致性
难易程度:一般
应用场景:
- 数据需要强一致性,绝对安全的场景,比如银行项目
优势:
- 保证了数据强一致性,绝对安全
- 将事务与业务解耦,持久化也放在了MQ中
缺点:
- 效率太低
1.2.2.4、流程中心(混合模式)
在过度设计中,我们曾提到过业务究竟是否需要回滚的问题。我们常用的分布式事务方法其实是一种流程的思维。因为在实际项目当中,分布式的事务的做法需要符合实际业务流程,而不能简单粗暴地有一条数据操作不成功就全局回滚。
比如订单与库存的分布式事务,当订单正常生成后,但库存不足(数据操作失败)。一般情况下,并不要求订单回滚(用户会很奇怪),而是应该把订单关闭,并记录库存异常,且通知采购。
其实大多数的分布式事务场景都是一种流程。流程中心的数据库记录了各个流程(包含各异常流程),其他后端程序与流程中心之间通过API调用。由于调度中心是独立子系统,所以即使某个业务子系统宕机,也能记录异常,以方便运营管理员通知运维人员修复。
而Seata的Sage模式,实际上就实现了流程中心的功能。
这种流程中心的做法不仅能解决大多数分布式事务场景,而且能切断子系统与子系统的直接联系。通过流程中心作为调度中心,既能保证各子系统的独立性,也能对一些复杂流程进行统一管理。而不会出现子系统间有缕不清的关系。
当然,这种流程的方式有一种场景是不合适的。就是同类数据分库分区记录的场景(数据分片存储),如两个用户记录在不同区域的不同数据库,A用户向B用户转账。对于这种场景,还是XA事务或者使用Seata框架比较合适。
流程中心的解决方案,以BASE理论的最终一致性为指导,并发性能非常高,是大厂常用的分布式事务解决方案。他是除XA强依赖于数据库原生事务之外能够混合使用的一种方案。
1.2.2.5、分布式事务解决方案总结
- 2PC
- XA:依赖于数据库原生事务
- Seata的AT:自动反解析SQL
- TCC:自定义准备、提交、回滚三个方案
- 持久化事务消息
- 本地事务表:强一致性
- MQ事务:解耦的本地事务表,强一致性
- 流程中心、3PC
- Seata的Sage:自定义状态引擎
1.3、总结
所谓的事务,不论是分布式也好,本地事务也好,它的本质就是保证数据的一致性。 在外部看来,一致性分为三种,强一致性、弱一致性和最终一致性。他们的区别就是在时间性上,强一致性需要你在最短甚至业务数据持久化的瞬间完成;弱一致性需要你在数据持久化完成之后的极短时间内完成数据的一致性;最终一致性意味着我们能够给予一致性在时间上更多的宽容度。
那么我们应该怎么使用保证一致性的事务呢?我认为事务分为两种,短事务和长事务。
分布式事务是一种业务设计不规范(过度设计),而造成的数据库分离,从而不得不使用分布式事务的场景。我们应该尽量避免强一致性的分布式事务场景。
强一致性的分布式事务会显著降低并发和吞吐量,而分布式系统本身就是为了提升并发和吞吐量,二者的矛盾会使得使用了强一致性分布式事务的分布式系统可能还不如单体系统,换而言之,使用了强一致性分布式事务解决方案的分布式系统毫无意义。
1.3.1、短事务
短事务意味着业务的流程并不长,并且业务之间的关联性比较强,它的特点就是需要保证强一致性
建议:强一致性的场景下,我们需要在设计上尽量避免分布式业务的生产,尽量合并使用一个数据库,从而使用本地事务。需要注意的是,这种场景下事务分布式事务的解决方案会造成分布式服务的吞吐量和并发大幅降低,有时还不如单体项目。
避免使用分布式事务的解决方案:同步调用;异步补偿;校验状态加锁;调用链路避免深长;服务聚合
- 同步调用,异步补偿,校验状态加锁目标是在业务上去保证一致性,优点是无需重构服务,无需改动数据库,缺点是程序员维护难度比较高
- 服务聚合的解决方案也就是将数据库合并,使用本地事务替代分布式事务,有点是极易维护,缺点是需要改动数据库(将两个数据库合并),可能还需要对两个服务进行重构
1.3.2、长事务
长事务意味着业务流程非常的漫长,并且还有可能存在调用第三方应用的情况,很明显他们业务关联性并不强,可能还是一些可有可无的业务。这种场景下,保证最终一致性甚至尽最大可能通知是最佳的解决方案。
建议:最终一致性场景下,我们也应该尽量避免强一致性的场景,采取业务的形式,可采用的解决方案是持久化事务消息和流程中心
- 持久化事务消息实际上是在请求入口处做一个持久化并记录流水号,之后通过业务的方式保证其最终一致性。 MQ的事务消息本质上是这种解决方案的增强。 优点是效率很高,缺点是开发成本太大,加入使用MQ,那么还需要与MQ中间件耦合。
- 流程中心例如seata的
saga
模式,采用外部编写的方法来控制事务流程。这种解决方案的本质还是业务流程解决一致性问题。优点是开发流程简单,易用性比较高,功能也更多,比如重试,复杂流程编排等。缺点是需要耦合流程中心,并且由流程中心控制(rpc)性能有一定的损耗。
评论区