导读概述
随着业务的快速发展,业务复杂度越来越高,大部分互联网公司几乎都会从单体走向分布式,特别是转向微服务架构,随之而来就必然遇到分布事务这个难题。
本文主要介绍一下Seata是如何保证数据一致性的,将从以下几个方面介绍一下分布式实现方案
1、介绍一下分布式理论(此部分如果已经掌握了可以跳过,看下面部分分享)
2、什么是Seata
3、Seata AT模式下如何解决分布式事务,应用场景
4、Seata TCC模式下如何解决分布式事务、应用场景、变种场景使用
5、Seata 下的Saga模式
本文以下单占库存案例为主线分享Seata分布式解决方案。
分布式理论
什么是事务?
指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。
使用事务目的是什么
说明白了就是保证数据的一致性。
什么是本地事务呢
我们常常指的是关系型数据库的事务,关系型数据库事务四大特性:
Atomicity(原子性) :要么都成功,要么都失败。
Consistency(一致性): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏,完整性包括外键约束、应用定义的等约束不会被破坏。
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
优点:可以保证单机数据库层面的数据一致性。
为什么要使用分布式事务
分布式事务在分布式环境下,为满足可用性、性能与降级服务的需要,降低一致性与隔离性的要求,一方面遵循 BASE 理论
基本业务可用性(Basic Availability)
柔性状态(Soft state)
最终一致性(Eventual consistency)
同样的,分布式事务也部分遵循 ACID 规范:
原子性:严格遵循
一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽
隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽
持久性:严格遵循
需求是这样的:
随着业务发展在微服务架构下,【订单服务】和【库存服务】在两个业务系统里,同时分别调用对应的订单DB和库存DB,要求保证:要么库存和订单下单一起成功,如果库存占用和订单下单有一方失败要么就要回滚,保证数据的一致性。
此时场景数据库级别的事务就有些捉襟见肘了,此时分布式事务就派上用场了。
分布式事务常见解决手段有哪些呢?
两阶段提交2PC、三阶段提交3PC、TCC、SAGA、本地消息表、基于可靠消息保证最终一致、最大努力通知等方案。
由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。
什么是分布式事务呢?
1、分布式事务:
- 首先应用于分布式系统场景,分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
- 例如在电商系统中,下单接口通常会扣减库存、添加优惠券、生成订单 id, 而订单服务与库存、添加优惠券、下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
2、常见使用场景:
如用户注册送积分事务、创建订单减库存事务,银行转账事务等都可以应用分布式事务,分布式事务就是为了保证在分布式场景下,数据操作的正确执行。
提到分布式场景就绕不开CAP理论,三者只能满足两点,来解决分布式场景的问题,那么接下来先了解一下什么是CAP。
CAP原则
CAP 原则又称 CAP 定理,又被叫作布鲁尔定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
CAP 原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。如果在某个分布式系统中数据无副本,那么系统必然满足强一致性条件,因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足。
即要保证数据一致还要保证可用,鱼和熊掌不可兼得,那么就要取舍,于是产生以下几种一致性策略。
强一致性、弱一致性、最终一致性
1.强一致性
任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。如:2PC、3PC、XA都属于强一致性方案。
2.弱一致性
数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
3.最终一致性 不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。如:TCC和消息队列模式、Saga模式属于最终一致性的解决方案。
了解了分布式事务中的强一致性和最终一致性理论,下面介绍几种常见的分布式事务的解决方案。
Seata 分布式事务
什么是Seata?
Seata是一款阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
什么是Seata 的AT模式?
AT模式的特点就是对业务无入侵式,整体机制分二阶段提交
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制:两阶段协议的演变
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成;回滚通过一阶段的回滚日志进行反向补偿。
Seata术语:
TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
Seata 的AT模式使用场景
怎么理解AT模式呢,咱们以占用库存和创建订单为例:
TM端功能包括【占用库存】和【创建订单】的操作,使用@GlobalTransaction进行全局事务开启、提交、回滚。
TM开始RPC调用远程服务调用分支【库存】服务和【创建订单】服务。
RM端seata-client通过扩展DataSourceProxy,实现自动生成undo_log与TC上报。
TM告知TC提交/回滚全局事务。
TC通知RM各自执行commit/rollback操作,同时清除undo_log。
下面为你介绍一下AT模式下的占库存和生单流程拆解如下:
第一阶段(占用库存过程)
- TM开启全局事务。
- 库存注册分支事务,获取全局锁。
- 本地事务提交:插入前镜像beforeImage->undolog日志,占用库存,插入后镜像afterImage->undolog日志,生成undo log一并提交,将本地事务提交的结果上报给TC。
第一阶段(创建订单过程)
- 创建订单注册分支事务,获取全局锁。
- 本地事务提交:插入前镜像beforeImage->undolog日志,创建订单成功,插入后镜像afterImage->undolog日志,生成undo log一并提交,将本地事务提交的结果上报给TC。
第二阶段:提交或回滚
- 场景1.如果占用库存成功 订单创建成功,则提交全局事务结束,删除本地undolog记录
- 场景2.如果占用库存或订单创建某一方失败,则反向操作undolog回滚日志,删除本地undlog记录。
AT模式下如何保证数据一致的?
一阶段提交如何保持一致性?
TM:method下单服务方法执行时,由于该方法具有@GlobalTranscational标志,该TM会向TC发起全局事务,生成XID(全局锁)。
RM写表的过程,Seata 会拦截业务SQL,首先解析 SQL 语义,在业务数据被更新前,将其保存成before image,然后执行业务SQL,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如何保持一致性的?
因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志。
异常阶段如何保持一致性?
异常:TM执行失败,通知TC全局回滚,TC此时通知所有的RM进行回滚,根据undo_log反向操作,使用before image还原业务数据,删除undo_log,但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理了。
什么是Seata 的TCC模式?
TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法
- 一阶段 :Try 资源的检测和预留。
- 二阶段 commit 或 rollback回滚行为。 Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功。 Cancel:预留资源释放。
相应的,TCC模式(try-confirm-cancel),不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用自定义的 prepare 逻辑,Try 资源的检测和预留。
- 二阶段 commit 行为:调用自定义的 commit 逻辑,执行的业务操作提交;要求 Try 成功Confirm 一定要能成功,失败则重试。
- 二阶段 rollback 行为:调用自定义的 rollback 逻辑,预留资源释放。
所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
Seata 的TCC模式使用场景
怎么理解TCC模式呢,咱们还以下单扣库存为例,Try 阶段去占库存,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务,拆解流程请看下图:
占用库存storage TCC流程图如下:
- try:冻结库存操作。
- confirm:占用库存操作。
- cancel:回滚事务,直接调用回滚库存接口。
订单系统 order TCC流程图如下:
- try: 新增订单-状态更新成【创建中】。
- confirm: try成功,下单操作成功,订单状态由【创建中】更新成【创建成功】。
- cancel: 回滚事务,删除该订单记录。
小结:
当占用库存成功同时订单下成功时,提交全局事务,当接入TCC模式,最重要的事情就是考虑如何将业务模型拆成2阶段,实现成TCC的3个方法,并且保证Try成功Confirm一定能成功。相对于AT模式,TCC模式对业务代码有一定的侵入性,但是TCC模式无AT模式的全局行锁,TCC性能会比AT模式高很多。
Seata 的TCC模式变种实现方案?
在说TCC变种简化变种方案前,先说一说现实的业务场景:
现实业务并不一定如我们所愿,在业务已成规模,订单系统 和 库存系统,都是在分布式场景,各自的业务接口,各自的DB表,如何保证数据一致呢?
已实现逻辑采用业务代码异常调用返向补偿实现的,伪代码如下:
try{
占用库存()
try{
下单方法()
}cach(){
释放库存();
取消订单();
}
}cach(){
释放库存();
}
库存服务:1.占用库存接口 2.释放库存接口。
订单服务:1.下单接口 2.取消订单接口。
流程图如下:
基于现有逻辑如何改造成分布式事务实现方案呢 ?
方案1:采用AT模式,需要做的工作:
在业务库 订单数据库和库存数据库 分别创建undo_log日志。
RM端(库存业务和订单业务代码)seata-client通过扩展DataSourceProxy,实现自动生成undo_log与tc上报,需要业务嵌入代码实现。
缺点:需要对库存业务库和订单业务库存分别存加 undo_log表,事务相对比较长。
方案2:采用Seata TCC模式
try阶段:提供冻结库存方法 和 预占用订单方法 需要改造库存表和订单表提供中间状态标识。
confirm阶段:提供占用库存方法和下单成功方法。
cancel阶段:提供返向 释放库存和取消订单方法。
缺点:业务代码要写库存锁定状态业务逻辑+订单创建中的中间态。
从上面两个方案不难看出都有一个通病,都需要业务代码改造,侵入强,耦合度高,能否有一种,即不需要业务代码改造成本又低的方案呢?
方案3:接下来的方案是在TCC模式上做了些简化:
即在try阶段:直接占库存成功和下单成功。
在confirm阶段:不做业务处理,直接返回true,认为是成功的。
rollback阶段:做返回的取消操作即可。
缺点:浪费了一次commit空提交 IO交互。
优点:业务侵入性小,可以适应现有业务场景,偶合度低。
简化版本的TCC实现方案的时序图如下:
Seata TCC分布式场景下如何保证幂等呢?
首先说一下什么是幂等?
幂等意思:通常指对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的.因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,造成业务脏数据,就需要保证幂等性。
如何保证幂等呢?
此时需要对服务设计时进行幂等控制,通常我们可以用事务xid或业务主键判重来控制,或采用业务唯一标识来做幂等,总之还是需要业务自己来保证幂等的。
Seata下的Saga模式?
1.Saga是一种补偿协议,Saga 理论出自 Hector & Kenneth 1987发表的论文 Sagas,其核心思想是将长事务拆分为多个本地短事务。
解决了什么问题?
2.Saga模式是seata提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
如图:T1T3都是正向的业务流程,都对应着一个冲正逆向补偿操作C1C3
适用场景:
- 业务流程长、业务流程多。
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。
优势:
- 一阶段提交本地事务,无锁,高性能。
- 事件驱动架构,参与者可异步执行,高吞吐。
- 补偿服务易于实现。
缺点:
Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。
seata分布式事务常见问题
常见问题答疑
问:Seata框架如何来保证事务的隔离性的?
答:因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。
1.读隔离:Seata(AT 模式)的默认全局隔离级别是读未提交,必须要求全局的读已提交 ,目前 Seata 的方式是通过select语句加for update代理方法增加@GlobalLock+@Transactional或@GlobalTransaction实现。
2.写隔离:
一阶段本地事务提交前,需要确保先拿到全局锁 。
拿不到全局锁 ,不能提交本地事务。
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
问:脏数据回滚失败如何处理?
答:1.脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他人工介入操作)。
2.关闭回滚时undo镜像校验,不推荐该方案。
注意事项:建议事前做好隔离保证无脏数据。
问:Seata支持哪些RPC框架?
答:1. AT 模式支持:Dubbo、Spring Cloud、Motan、GRPC 和 Sofa-RPC。
2. TCC 模式支持:Dubbo、Spring Cloud和Sofa-RPC。
问:Seata现阶段支持哪些分库分表解决方案?
答:现阶段只支持ShardingSphere。
问:Seata目前支持高可用吗?
答:支持
1.tc使用db模式共享全局事务会话信息,注册中心使用非file的seata支持的第三方注册中心。
2.注册中心包含:eureka、consul、nacos、etcd、zookeeper、sofa、redis、file (直连)。
小结:
在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。
目前使用的流行度情况是:AT>TCC > Saga,Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的标准化平台。基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。
分布式方案对比
Seata针对不同的业务场景提供了四种不同的事务模式,对比如下:
AT模式:AT 模式的一阶段、二阶段提交和回滚(借助undo_log表来实现)均由 Seata 框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
TCC模式:相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT模式高很多。适用于核心系统等对性能有很高要求的场景。
SAGA模式:Sage 是长事务解决方案,事务驱动,使用那种存在流程审核的业务场景。
XA模式:XA模式是分布式强一致性的解决方案,但性能低而使用较少。
后记
本文介绍了分布式事务的一些基础理论,主要对Seata分布式事务方案进行了讲解,在文章的后半部分主要给出了各种方案的常用场景。分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要根据自身业务特点自行选择,每种方案在实际执行过程中需要考虑的点都非常多,复杂度较大,所以在非必要的情况下,分布式事务能不用就尽量不用,希望对大家有所帮助。