文章目录
- 1. 事务概述
- 1.1 本地事务
- 1.2 分布式事务
- 2. 分布式事务解决方案
- 2.1 两阶段提交型(2PC)
- 2.2 三阶段提交型(3PC)
- 2.3 TCC补偿型
- 2.4 最终一致性型
- 2.5 最大努力通知型事务
- 3. Seata概述
- 3.1 AT事务模式
- 3.2 XA模式
- 3.3 TCC模式
- 3.4 SAGA模式
- 3.5 四种模式对比
- 4. Seata的实践
- 4.1 下载与配置seata-server
- 4.2 引入依赖
- 4.3 配置TC地址
- 4.4 实现分布式事务
1. 事务概述
事务是数据库操作的一个逻辑单位,可以是一个或多个数据库操作的集合,在一个事务里,要么所有操作都执行成功,要么所有操作都不执行。
而事务又分为两种:
- 本地事务
- 分布式事务
1.1 本地事务
本地事务就是传统的单机事务,在传统的单机事务中,需要满足四个原则:
- 原子性:事务中的所有操作,要么一起成功,要么一起失败,不存在一部分成功一部分失败的情况;
- 一致性:数据库中的数据必须保持一致性,比如银行的总额度为100,经过转账操作后额度应该仍为100。数据库只能由一个一致性到另一个一致性的转变;
- 隔离性:多个事务之间都是独立的,一个未完成的事务不会影响到另一个未完成的事务。
- 持久性:一个事务一旦被提交,它会保持永久性,所更改的数据都会被写入到磁盘做持久化处理,就算
MySQL
宕机也不会影响数据改变,因为宕机后也可以通过日志恢复数据
1.2 分布式事务
分布式事务是指不是在单体或单个数据库架构下产生的事务
在微服务架构下,通常一个服务只处理一类事情,并且部署在一个服务节点上。
但是一个接口往往需要多个服务的支持,比如一个下单接口,需要经过扣减库存、生成订单、为用户添加积分等操作。
这里就涉及了三个服务,只有三个服务全部成功才说明本次下单成功,否则视为本次下单不成功。
为了保证每个服务的一致性,这里就不能使用单机事务了,需要使用分布式事务。
既然说到分布式事务,就不得不了解一下微服务的CAP理论了
- C(Consistency):一致性。服务A、B、C都存储了用户数据,三个节点数据都需要保持同一时刻数据的一致性。
- A(Availability):可用性。服务A、B、C三个节点,如果其中一个节点宕机了,不得影响整个集群对外服务。
- P(Partition Tolerance):分区容错性就是允许系统通过网络协同工作,分区容错性要解决由于网络分区导致数据的不完整及无法访问等问题。
但是三者不可同时兼得,对于CAP来说,只能在其中选择两个,也就是不是CA,就是AP,或是CP。
这就是大名鼎鼎的BASE理论
,它是用于对CAP理论进行一些补充的。
- BA(Basically Available):基本可用
- S(Soft State):软状态
- E(Eventually Consistent):最终一致性
这个理论的核心思想便是:如果我们如法做到强一致性,那么每个应用都应该根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
2. 分布式事务解决方案
分布式事务是解决分布式环境下的一致性问题,以及当数据量增加时需要做分库分表,这时候产生的数据一致性问题。
分布式事务的解决方案一般有以下几种:
- 两阶段提交型(2PC)
- 三阶段提交型(3PC)
- TCC补偿性
- 最终一致性型
- 最大努力通知型
2.1 两阶段提交型(2PC)
想要实现分布式事务,其实就是为了解决这样的一个问题,每个服务都知道自己的操作是否成功,但是却不知道别的服务操作是否成功,于是就需要一个协调者作为中间人,这个中间人统一掌握所有服务,协调者知道所有节点的执行结果并最终确认并指示相应的节点是否执行最后提交。
而两阶段提交就是说将整个事务处理的过程分为两个阶段,分别是准备阶段和提交阶段
这是基于XA协议实现的分布式事务,XA协议中分成了两个部分:事务管理器和本地资源管理器。
本地资源管理器由数据库实现,比如MySQL都实现了XA接口,而事务管理器则作为一个全局的调度者。
两阶段提交(2PC
),对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。
两阶段提交的第一阶段,协调者向所有参与者发送请求,询问它们是否可以进行事务提交
- 协调者向所有参与者发送事务的预提交请求。
- 每个参与者在接收到请求后,执行本地事务的验证和准备工作。如果该参与者可以进行事务的提交,就将事务日志记录为“待提交”状态。
- 参与者完成本地事务的验证和准备工作后,向协调者发送响应信息,表示自己是否可以提交事务。
- 协调者收到所有参与者的响应信息后,进行判断。如果所有参与者都响应“可以提交”,则进入第二阶段;否则,协调者发送回滚请求给所有参与者,中止事务。
第二阶段是提交(Commit)阶段。在该阶段,协调者根据第一阶段的反馈结果,决定是否进行事务的提交。
- 协调者在接收到所有参与者的预提交响应后,如果没有收到拒绝提交的响应,将向所有参与者发送提交请求。
- 每个参与者在接收到提交请求后,执行事务的最终提交操作。如果参与者在第一阶段已经将本地事务标记为“待提交”,那么它会在这一阶段将事务真正提交,将事务状态更新为“已提交”。
- 参与者完成事务的提交后,向协调者发送响应信息,表示自己已经完成事务的提交。
- 协调者等待所有参与者的提交响应,并在收到所有响应后,根据情况给出最终的事务提交结果。
- 如果所有参与者都提交成功,协调者向所有参与者发送提交完成的通知,并将事务状态更新为“已提交”。
- 如果任何一个参与者提交失败,协调者向所有参与者发送回滚请求,中止事务,并将事务状态更新为“已中止”。
可见,这是一个强一致性的同步阻塞协议,事务执行过程中需要把所需资源全部锁住,任何其他事务不可操作这些资源,所以只适合执行时间比较短的事务,整体性能比较差。
并且一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。
2.2 三阶段提交型(3PC)
三阶段提交型(3PC)是二阶段提交型(3PC)的一种改进版本,是为了解决2PC出现的提交协议的阻塞问题。
2PC最大的问题就是过度依赖事务协调者,一旦事务协调者出现问题,那么参与者就无法做出最后的选择,随后导致阻塞资源锁定资源。
在2PC中事务协调者存在超时机制,但是参与者并不存在超时机制:
- 预提交超时:在第一阶段的预提交阶段,如果协调者发送了预提交请求后一定时间内没有收到所有参与者的响应,就可以认为有参与者发生故障或网络异常。此时,协调者可以选择中止事务,并向所有参与者发送中止请求,以避免长时间等待和资源浪费。
- 提交超时:在第二阶段的提交阶段,如果协调者发送了提交请求后一定时间内没有收到所有参与者的提交响应,也可以认为有参与者发生故障或网络异常。此时,协调者可以选择中止事务,并向所有参与者发送回滚请求,以确保数据的一致性。
- 参与者超时:在参与者收到协调者的请求后,如果没有在一定时间内完成本地事务的验证、准备或提交操作,可以将其视为参与者发生故障或处理过程出现异常。此时,参与者可以向协调者发送超时响应,通知其发生异常情况,并由协调者来决定如何继续处理事务。
于是,在3PC中事务协调者和参与者都引入了超时机制,并且在第一第二阶段又插入了一个准备阶段,当事务协调者出现故障时,参与者就不会一直阻塞,保证了在最后提交阶段之前各参与节点的状态是一致的。
三阶段提交的过程如下:
- 准备阶段(CanCommit):协调者向参与者发送准备请求,询问它们是否可以提交事务。参与者执行本地事务验证,并将结果返回给协调者。
- 如果所有参与者都准备就绪且同意提交,则进入第二阶段。
- 如果任何一个参与者拒绝提交,或者在超时时间内没有收到所有参与者的响应,协调者将会中止事务。
- 执行阶段(PreCommit):协调者向参与者发送预提交请求,要求它们执行事务的最终提交操作。参与者在此阶段执行实际的提交操作,并将结果返回给协调者。
- 如果所有参与者成功执行了事务的提交操作,则进入第三阶段。
- 如果任何一个参与者提交失败,或者在超时时间内没有收到所有参与者的响应,协调者将会中止事务。
- 提交阶段(DoCommit):协调者向参与者发送最终提交请求,要求它们正式提交事务。参与者在此阶段将提交操作持久化,并向协调者发送响应。
- 如果所有参与者成功完成了事务的最终提交,则事务最终提交成功。
2.3 TCC补偿型
TCC是指Try
、Confirm
、Cancel
三个操作
Try
:预留业务资源;Confirm
:确认执行业务操作;Cancel
:取消执行业务操作。
该方案的大体步骤如下:
- 通过
Try
先锁住对象资源进行预留操作,只要预留资源这一步成功了,才会有后面的操作; - 资源预留成功后,执行
Confirm
操作,也就是对Try
阶段锁定的资源进行业务操作; - 如果
Confirm
执行失败,则执行Cancel
操作,也就是在所有操作失败时的回滚操作,释放预留资源。
对于TCC补偿型来说,不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则通过Cancel
阶段进行回滚补偿,也就是补偿性事务。
但是TCC的代码侵入性非常强,因为原本一个方法但如今需要三个方法来完成。并且这种模式的代码不能很好被复用,会导致开发量增加。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
2.4 最终一致性型
最终一致性型,也成为消息事务,这是因为这种方案是基于MQ实现的,将本地事务和发消息放在同一个事务里,保证本地操作和发消息同时成功。
- 订单系统向
MQ
发送一条预备扣减库存消息,MQ
保存预备消息并返回成功ACK
- 接收到预备消息执行成功
ACK
,订单系统执行本地下单操作,为防止消息发送成功而本地事务失败,订单系统会实现MQ
的回调接口,其内不断的检查本地事务是否执行成功,如果失败则rollback
回滚预备消息;成功则对消息进行最终commit
提交。 - 库存系统消费扣减库存消息,执行本地事务,如果扣减失败,消息会重新投,一旦超出重试次数,则本地表持久化失败消息,并启动定时任务做补偿。
基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况。
2.5 最大努力通知型事务
最大努力通知型事务(Best-Effort Delivery Transaction)是一种分布式系统中常见的事务模型。在这种模型下,系统不保证对所有参与者的通知操作一定会成功到达,而是采取尽最大努力去通知参与者。
最大努力通知型事务的特点如下:
- 最多一次通知:对每个参与者来说,可能只能收到一次通知,也可能根本不会收到通知。因此,参与者需要有处理重复通知的机制,以确保业务的正确性。
- 消息丢失:通知消息在分布式系统中可能会因为网络故障、参与者故障等原因丢失。即使通知消息丢失了,系统也不会进行重试或者补偿操作。
- 无超时或回滚:最大努力通知型事务并没有像两阶段提交或三阶段提交那样的超时机制或回滚机制。一旦通知消息发送出去,就无法撤销或者取消。
最大努力通知型事务常见于异步通信场景,其中通知操作的可靠性要求相对较低,可以容忍一定的失败率。例如,使用消息队列进行异步通知,发布者将通知消息发送给订阅者,但无法保证每个订阅者都能够成功接收到通知。
在设计最大努力通知型事务时,需要考虑以下几点:
- 业务容错性:系统需要具备处理重复通知的机制,以避免重复执行业务操作导致数据不一致或错误。
- 监控和日志记录:为了追踪通知操作的结果,系统应该有相应的监控和日志记录机制,以便后续对通知结果进行分析和处理。
- 异常处理:当通知操作失败时,需要有相应的异常处理机制,例如重试、补偿或人工干预等,以保证业务的正常进行。
最大努力通知型事务适用于那些对实时性要求不高,而且可以容忍一定通知失败率的业务场景。对于一些对可靠性和一致性要求更高的场景,可能需要选择其他更强大的事务模型或协议来保证数据的一致性和可靠性。
支付回调就是类似的原理,支付接口都需要一个回调地址,在支付成功后,支付接口提供方会将支付结果返回到我们的回调地址,如果没有收到支付成功的通知,支付接口提供方会重复调用我们的接口,直到通知指定次数后不再通知。
3. Seata概述
Seata
是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。官网:Seata | Seata
Seata
也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT
、TCC
、SAGA
和 XA
等事务模式。
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
Seata
分布式事务的几个角色:
-
Transaction Coordinator(TC)
: 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交。 -
Transaction Manager™
: 事务管理者,业务层中用来开启/提交/回滚一个整体事务(在调用服务的方法中用注解开启事务)。 -
Resource Manager(RM)
: 资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction
),理分支事务与TC
进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。
Seata
实现分布式事务,设计了一个关键角色UNDO_LOG
(回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在UNDO_LOG
表中,以便业务异常能随时回滚。
3.1 AT事务模式
AT事务模式采用了类似于数据库中的乐观锁模式来实现分布式事务控制。在分布式事务发生时,Seata
会对涉及到的所有资源进行代理,并将它们作为一个整体进行管理。
比如需要更新User表的一个name字段。
对应的就是以下这样的一个SQL
update user set name = 'IKUN' where name = '鸡你太美'
这时候,在Seata
的第一阶段,Seata
的JDBC
数据源代理通过业务SQL
解析,提取出SQL
的元数据。元数据也就是SQL
的类型、表、条件等。
接着查询数据前镜像,根据解析得到的元数据生成查询SQL,定位一条或多条数据。
select name from user where name = '鸡你太美'
ID | NAME | USER_ID |
---|---|---|
1 | 鸡你太美 | 123 |
紧接着执行业务 SQL,根据前镜像数据主键查询出后镜像数据
select name from user where id = 1
ID | NAME | USER_ID |
---|---|---|
1 | IKUN | 123 |
接着,将业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和 UNDO_LOG
表中。
这时候回滚记录数据格式如下:包括前镜像,后镜像,分支事务ID,全局事务ID。
在本地事务提交前,各分支事务需向
全局事务协调者
TC 注册分支 (Branch Id
) ,为要修改的记录申请 全局锁 ,要为这条数据加锁,利用SELECT FOR UPDATE
语句。而如果一直拿不到锁那就需要回滚本地事务。TM 开启事务后会生成全局唯一的XID
,会在各个调用的服务间进行传递。
有了这样的机制,本地事务分支便可以在全局事务的第一阶段提交,并立马释放本地事务锁定的资源。
相比与XA机制
,Seata
降低了锁的范围,提高了效率,即使第二阶段出现异常,也可以通过UNDO_LOG
表找到对应的数据并解析成SQL进行回滚。
最后本地事务提交,业务数据的更新和前面生成的 UNDO LOG 数据一并提交,并将本地事务提交的结果上报给全局事务协调者 TC。
在Seata
的第二阶段,则是根据各分支的决议做提交或回滚。
- 如果决议为提交,此时各分支事务已提交并成功,这时
全局事务协调者(TC)
会向分支发送第二阶段的请求。收到 TC 的分支提交请求,该请求会被放入一个异步任务队列中,并马上返回提交成功结果给 TC。异步队列中会异步和批量地根据Branch ID
查找并删除相应UNDO LOG
回滚记录。 - 如果决议是全局回滚,过程比全局提交麻烦一点,
RM
服务方收到TC
全局协调者发来的回滚请求,通过XID
和Branch ID
找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
注意:这里的删除日志,必须是在事务执行之后删除。
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
3.2 XA模式
Seata
对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM一阶段的工作:
① 注册分支事务到TC
② 执行分支业务sql但不提交
③ 报告执行状态到TC
TC二阶段的工作:
-
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务
RM二阶段的工作:
- 接收TC指令,提交或回滚事务
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
3.3 TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
-
Try
:资源的检测和预留; -
Confirm
:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。 -
Cancel
:预留资源释放,可以理解为try的反向操作。
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
- 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初识余额:
余额充足,可以冻结:
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
- 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
3.4 SAGA模式
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
3.5 四种模式对比
4. Seata的实践
4.1 下载与配置seata-server
首先,我们需要下载seata-server包,地址在http😕/seata.io/zh-cn/blog/download.html
接着,解压这个压缩包
修改conf目录下的registry.conf文件:
registry {
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
4.2 引入依赖
在服务中引入Seata
依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<!--seata starter 采用1.4.2版本-->
<version>${seata.version}</version>
</dependency>
4.3 配置TC地址
在application.yml
配置TC服务信息,并通过nacos结合服务名称获取TC地址
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-tc-server # seata服务名称
username: nacos
password: nacos
# 分布式事务的模式,默认不写为AT
data-source-proxy-mode: AT
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: SH
4.4 实现分布式事务
只需要使用@GlobalTransactional
注解即可实现分布式事务。
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
参考:
- 看了 5种分布式事务方案,我司最终选择了 Seata,真香! - 掘金 (juejin.cn)
- 分布式事务解决方案之 Seata(一):分布式事务的常见解决方案及 Seata 简介 - 掘金 (juejin.cn)
- 从分布式事务解决到Seata使用,一梭子给你整明白了 - 掘金 (juejin.cn)
- 【深入浅出Seata原理及实战】「入门基础专题」带你透析认识Seata分布式事务服务的原理和流程(1) - 掘金 (juejin.cn)
- 分布式事务解决方案-seata - 掘金 (juejin.cn)
- Seata | Seata