分布式事务简介(seata)
一、简介
1、本地事务与分布式事务
1.1 事务
数据库事务(简称:事务,Transaction)是指数据库执⾏过程中的⼀个逻辑单位,由⼀个有限的数据库操作序列构成。
事务拥有以下四个特性,习惯上被称为ACID特性:
原⼦性(Atomicity): 事务作为⼀个整体被执⾏,包含在其中的对数据库的操作要么全部被执⾏,要么都不执⾏。
⼀致性(Consistency): 事务应确保数据库的状态从⼀个⼀致状态转变为另⼀个⼀致状态。⼀致状态是指数据库中的数据应满⾜完整性约束。除此之外,⼀致性还有另外⼀层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原⼦性)。
隔离性(Isolation): 多个事务并发执⾏时,⼀个事务的执⾏不应影响其他事务的执⾏,如同只有这⼀个操作在被数据库所执⾏⼀样。
持久性(Durability): 已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。
1.2 本地事务
起初,事务仅限于对单⼀数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将⼀个单⼀的服务操作作为⼀个事务,那么整个服务操作只能涉及⼀个单⼀的数据库资源,这类基于单个服务单⼀数据库资源访问的事务,被称为本地事务(Local Transaction)。
![请添加图片描述](https://img-blog.csdnimg.cn/2ddf5e17febd42829fd54abe6c74dfb7.png)
1.3 分布式事务
分布式事务指事务的参与者、⽀持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应⽤,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据⼀致性。当⼀个服务操作访问不同的数据库资源,⼜希望对它们的访问具有事务特性时,就需要采⽤分布式事务来协调所有的事务参与者。
下图反映了这样⼀个跨越多个服务的分布式事务:
![在这里插入图片描述](https://img-blog.csdnimg.cn/50880db87b0d4059a8a46c58e6222fa3.png)
如果将上⾯这两种场景(⼀个服务可以调⽤多个数据库资源,也可以调⽤其他服务)结合在⼀起,对此进⾏延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。
![请添加图片描述](https://img-blog.csdnimg.cn/c15ce6e266ee4f3391a3b1df800fbc69.png)
2、分布式事务相关理论
2.1 CAP定理
![请添加图片描述](https://img-blog.csdnimg.cn/a9055912521e42f58fd8dcc494cb4977.png)
CAP定理是在 1998年加州⼤学的计算机科学家 Eric Brewer (埃⾥克.布鲁尔)提出,分布式系统有三个指标,这三个指标不可能同时做到。
- Consistency ⼀致性
- Availability 可⽤性
- Partition tolerance 分区容错
分区容错(Partition tolerance)
分布式系统集群中,⼀个机器坏掉不应该影响其他机器
⼤多数分布式系统都分布在多个⼦⽹络。每个⼦⽹络就叫做⼀个区(partition)。分区容错的意思是,区间通信可能失败。⽐如,⼀台服务器放在中国,另⼀台服务器放在美国,这就是两个区,它们之间可能⽆法通信。
⼀般来说,分区容错⽆法避免,因此可以认为 CAP 的 P 总是成⽴。CAP 定理告诉我们,剩下的 C 和 A ⽆法同时做到。
可⽤性(Availability)
只要收到⽤户的请求,服务器就必须给出回应⽤户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉⽤户,到底是 v0 还是v1,否则就不满⾜可⽤性。
⼀致性(Consistency)
⼀定能读取到最新的数据
Consistency 中⽂叫做⼀致性
。意思是,写操作之后的读操作,必须返回该值。
⼀致性和可⽤性的⽭盾
⼀致性©和可⽤性(A),为什么不可能同时成⽴?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的⼀致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可⽤性(CP)。
如果保证 G2 的可⽤性,那么势必不能锁定 G2,所以⼀致性不成⽴(AP)。
综上所述,G2 ⽆法同时做到⼀致性和可⽤性。系统设计时只能选择⼀个⽬标。如果追求⼀致性,那么⽆法保证所有节点的可⽤性;如果追求所有节点的可⽤性,那就没法做到⼀致性。
2.2 Base理论
BASE:全称:Basically Available(基本可⽤),Soft state(软状态),和 Eventually consistent(最终⼀致性)三个短语的缩写,来⾃ ebay 的架构师提出。BASE 理论是对 CAP 中⼀致性和可⽤性权衡的结果,其来源于对⼤型互联⽹分布式实践的总结,是基于 CAP 定理逐步演化⽽来的。其核⼼思想是:
既是⽆法做到强⼀致性(Strong consistency),但每个应⽤都可以根据⾃身的业务特点,采⽤适当的⽅式来使系统达到最终⼀致性(Eventual consistency)。
Basically Available(基本可⽤)
理解: 允许服务降级或者允许响应时间受到⼀定损失
什么是基本可⽤呢?假设系统,出现了不可预知的故障,但还是能⽤,相⽐较正常的系统⽽⾔:
- 响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给⽤户结果,⽽基本可⽤的搜索引擎可以在 1 秒作⽤返回结果。
- 功能上的损失:在⼀个电商⽹站上,正常情况下,⽤户可以顺利完成每⼀笔订单,但是到了⼤促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到⼀个降级⻚⾯。
Soft state(软状态)
理解: 允许同步数据的时候出现⼀定时间延迟
什么是软状态呢?相对于原⼦性⽽⾔,要求多个节点的数据副本都是⼀致的,这是⼀种 “硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可⽤性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent(最终⼀致性)
理解: 经过⼀段时间的同步数据之后,最终都能够达到⼀个⼀致的状态。
系统能够保证在没有其他新的更新操作的情况下,数据最终⼀定能够达到⼀致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。
3、分布式事务解决方案
3.1 基于XA协议的两阶段提交
![在这里插入图片描述](https://img-blog.csdnimg.cn/684fffaaf73e4fb18aa247865dc183a0.png)
可知XA规范中分布式事务有AP,RM,TM组成:
其中应⽤程序(Application Program ,简称AP): AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
资源管理器(Resource Manager,简称RM): Rm管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含⽐如数据库、⽂件系统、打印机服务器等。
事务管理器(Transaction Manager ,简称TM): 负责管理全局事务,分配事务唯⼀标识,监控事务的执⾏进度,并负责事务的提交、回滚、失败恢复等。
⼆阶段协议:
第⼀阶段TM: 要求所有的RM准备提交对应的事务分⽀,询问RM是否有能⼒保证成功的提交事务分⽀,RM根据⾃⼰的情况,如果判断⾃⼰进⾏的⼯作可以被提交,那就对⼯作内容进⾏持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经的⼯作后,就可以丢弃这个事务分⽀信息了。
第⼆阶段TM: 根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进⾏提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚⾃⼰的事务分⽀。
也就是TM与RM之间是通过两阶段提交协议进⾏交互。
优点: 尽量保证了数据的强⼀致,适合对数据强⼀致要求很⾼的关键领域。(其实也不能100%保证强⼀致)
缺点: 实现复杂,牺牲了可⽤性,对性能影响较⼤,不适合⾼并发⾼性能场景。
3.2 TCC补偿机制
TCC 其实就是采⽤的补偿机制,其核⼼思想是:针对每个操作,都要注册⼀个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留。
- Confirm 阶段主要是对业务系统做确认提交,Try阶段执⾏成功并开始执⾏ Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm⼀定成功。
- Cancel 阶段主要是在业务执⾏错误,需要回滚的状态下执⾏的业务取消,预留资源释放。
![请添加图片描述](https://img-blog.csdnimg.cn/563072dd35ab4fe3948e3793753ac742.png)
A要向 B 转账,思路⼤概是:
我们有⼀个本地⽅法,⾥⾯依次调⽤
1、⾸先在 Try 阶段,要先调⽤远程接⼝把 B和 A的钱给冻结起来。
2、在 Confirm 阶段,执⾏远程调⽤的转账的操作,转账成功进⾏解冻。
3、如果第2步执⾏成功,那么转账成功,如果第⼆步执⾏失败,则调⽤远程冻结接⼝对应的解冻⽅法 (Cancel)。
优点: 相⽐两阶段提交,可⽤性⽐较强
缺点: 数据的⼀致性要差⼀些。TCC属于应⽤层的⼀种补偿⽅式,所以需要程序员在实现的时候多写很多补偿的代码,在⼀些场景中,⼀些业务流程可能⽤TCC不太好定义及处理。
3.3 消息最终一致性
消息最终⼀致性应该是业界使⽤最多的,其核⼼思想是将分布式事务拆分成本地事务进⾏处理,这种思路是来源于ebay。我们可以从下⾯的流程图中看出其中的⼀些细节:
![请添加图片描述](https://img-blog.csdnimg.cn/7d74bf4172504cc98e371fb78937019b.png)
基本思路就是:
消息⽣产⽅,需要额外建⼀个消息表,并记录消息发送状态。消息表和业务数据要在⼀个事务⾥提交,也就是说他们要在⼀个数据库⾥⾯。然后消息会经过MQ发送到消息的消费⽅。如果消息发送失败,会进⾏重试发送。
消息消费⽅,需要处理这个消息,并完成⾃⼰的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执⾏。如果是业务上⾯的失败,可以给⽣产⽅发送⼀个业务补偿消息,通知⽣产⽅进⾏回滚等操作。
⽣产⽅和消费⽅定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送⼀遍。如果有靠谱的⾃动对账
补账逻辑,这种⽅案还是⾮常实⽤的。
优点: ⼀种⾮常经典的实现,避免了分布式事务,实现了最终⼀致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决⽅案,会有很多杂活需要处理。
二、分布式事务 seata
1、简介
Seata(原名Fescar) 是阿⾥18年开源的分布式事务的框架。Fescar的开源对分布式事务框架领域影响很⼤。作
为开源⼤户,Fescar来⾃阿⾥的GTS,经历了好⼏次双⼗⼀的考验,⼀经开源便颇受关注。后来Fescar改名为
Seata。
Fescar虽然是⼆阶段提交协议的分布式事务,但是其解决了XA的⼀些缺点:
-
单点问题: 虽然⽬前Fescar(0.4.2)还是单server的,但是Fescar官⽅预计将会在0.5.x中推出HA-Cluster,到时候就可以解决单点问题。
-
同步阻塞: Fescar的⼆阶段,其再第⼀阶段的时候本地事务就已经提交释放资源了,不会像XA会再两个prepare和commit阶段资源都锁住,并且Fescar,commit是异步操作,也是提升性能的⼀⼤关键。
-
数据不⼀致: 如果出现部分commit失败,那么fescar-server会根据当前的事务模式和分⽀事务的返回状态的结果来进⾏不同的重试策略。并且fescar的本地事务会在⼀阶段的时候进⾏提交,其实单看数据库来说在commit的时候数据库已经是⼀致的了。
-
只能⽤于单⼀数据库: Fescar提供了两种模式,AT和TCC。在AT模式下事务资源可以是任何⽀持ACID的数据库,在TCC模式下事务资源没有限制,可以是缓存,可以是⽂件,可以是其他的等等。当然这两个模式也可以混⽤。
同时Fescar也保留了接近0业务⼊侵的优点,只需要简单的配置Fescar的数据代理和加个注解,加⼀个Undolog表,就可以达到我们想要的⽬的。
2、实现原理
Fescar将⼀个本地事务做为⼀个分布式事务分⽀,所以若⼲个分布在不同微服务中的本地事务共同组成了⼀个全局事务,结构如下。
![请添加图片描述](https://img-blog.csdnimg.cn/a52acee8181b4671b42d8a1e6c720204.png)
TM: 全局事务管理器,在标注开启fescar分布式事务的服务端开启,并将全局事务发送到TC事务控制端管理
TC: 事务控制中⼼,控制全局事务的提交或者回滚。这个组件需要独⽴部署维护,⽬前只⽀持单机版本,后续迭代计划会有集群版本
RM: 资源管理器,主要负责分⽀事务的上报,本地事务的管理
⼀段话简述其实现过程:
服务起始⽅发起全局事务并注册到TC。
在调⽤协同服务时,协同服务的事务分⽀事务会先完成阶段⼀的事务提交或回滚,并⽣成事务回滚的undo_log⽇志,
同时注册当前协同服务到TC并上报其事务状态,归并到同⼀个业务的全局事务中。
此时若没有问题继续下⼀个协同服务的调⽤,期间任何协同服务的分⽀事务回滚,都会通知到TC,TC在通知全局事务包含的所有已完成⼀阶段提交的分⽀事务回滚。
如果所有分⽀事务都正常,最后回到全局事务发起⽅时,也会通知到TC,TC在通知全局事务包含的所有分⽀删除回滚⽇志。
在这个过程中为了解决写隔离和度隔离的问题会涉及到TC管理的全局锁。
3、Fescar模式
Fescar对分布式事务的实现提供了2种模式,AT模式和TCC模式:
3.1 TCC模式
TCC模式: TCC补偿机制,对代码造成⼀定的侵⼊,实现难度较⼤,这种⽅式不推荐,不过TCC模式的特点是性能⾼。
TCC模式部分代码如下: 可以看到执⾏事务回滚,都需要根据不同阶段执⾏的状态判断,侵⼊了业务代码。
@Override
@GlobalTransactional
public boolean transfer(final String from, final String to, final double amount) {
//扣钱参与者,⼀阶段执⾏
boolean ret = firstTccAction.prepareMinus(null, from, amount);
if (!ret) {
//扣钱参与者,⼀阶段失败; 回滚本地事务和分布式事务
throw new RuntimeException("账号:[" + from + "] 预扣款失败");
}
//加钱参与者,⼀阶段执⾏
ret = secondTccAction.prepareAdd(null, to, amount);
if (!ret) {
throw new RuntimeException("账号:[" + to + "] 预收款失败");
}
System.out.println(String.format("transfer amount[%s] from [%s] to [%s] finish.", String.valueOf(amount), from, to));
return true;
}
3.2 AT模式
AT模式: 主要关注多 DB 访问的数据⼀致性,实现起来⽐较简单,对业务的侵⼊较⼩,但性能没有TCC⾼,这种模式推荐⼤家使⽤。
AT模式部分代码如下: 不需要关注执⾏状态,对业务代码侵⼊较⼩。
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
storageService.deduct(commodityCode, orderCount);
orderService.create(userId, commodityCode, orderCount);
throw new RuntimeException("AT 模式发⽣异常,回滚事务");
}
3.3 AT 模式设计思路
AT模式的核⼼是对业务⽆侵⼊,是⼀种改进后的两阶段提交。
第一阶段:
![请添加图片描述](https://img-blog.csdnimg.cn/e2f59c3bcda444e6ae8a2b8e2ee48b75.png)
核⼼在于对业务sql进⾏解析,转换成undolog,两阶段提交往往对资源的锁定需要持续到第⼆阶段实际的提交或者回滚操作,⽽有了回滚⽇志之后,可以在第⼀阶段释放对资源的锁定,降低了锁范围,提⾼效率,即使第⼆阶段发⽣异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚⽬的。Seata通过代理数据源将业务sql的执⾏解析成undolog来与业务数据的更新同时⼊库,达到了对业务⽆侵⼊的效果。
第二阶段:
如果决议是全局提交,此时分⽀事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚⽇志),Phase2 可以⾮常快速地完成。
![请添加图片描述](https://img-blog.csdnimg.cn/20ff50b9f4894231b529828b56935648.png)
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚⽇志记录,通过回滚记录⽣成反向的更新 SQL 并执⾏,以完成分⽀的回滚。
![请添加图片描述](https://img-blog.csdnimg.cn/287872a811e34e4385235cfb96099abe.png)
4、官方信息
https://seata.io/zh-cn/
https://github.com/seata/seata
5、快速开始
pom.xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
代码:
package com.work.order.service;
import com.work.order.model.Order;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class OrderService {
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public void placeOrder(String userId, String commodityCode, Integer count) {
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
orderMoney);
orderDAO.insert(order);
stockFeignClient.deduct(commodityCode, count);
}
}