事务是数据库操作的一个逻辑单位,可以是一个或多个数据库操作的集合,在一个事务里,要么所有操作都执行成功,要么所有操作都不执行。
而事务又分为两种:
本地事务就是传统的单机事务,在传统的单机事务中,需要满足四个原则:
MySQL
宕机也不会影响数据改变,因为宕机后也可以通过日志恢复数据分布式事务是指不是在单体或单个数据库架构下产生的事务
在微服务架构下,通常一个服务只处理一类事情,并且部署在一个服务节点上。
但是一个接口往往需要多个服务的支持,比如一个下单接口,需要经过扣减库存、生成订单、为用户添加积分等操作。
这里就涉及了三个服务,只有三个服务全部成功才说明本次下单成功,否则视为本次下单不成功。
为了保证每个服务的一致性,这里就不能使用单机事务了,需要使用分布式事务。
既然说到分布式事务,就不得不了解一下微服务的CAP理论了
但是三者不可同时兼得,对于CAP来说,只能在其中选择两个,也就是不是CA,就是AP,或是CP。
这就是大名鼎鼎的BASE理论
,它是用于对CAP理论进行一些补充的。
这个理论的核心思想便是:如果我们如法做到强一致性,那么每个应用都应该根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
分布式事务是解决分布式环境下的一致性问题,以及当数据量增加时需要做分库分表,这时候产生的数据一致性问题。
分布式事务的解决方案一般有以下几种:
想要实现分布式事务,其实就是为了解决这样的一个问题,每个服务都知道自己的操作是否成功,但是却不知道别的服务操作是否成功,于是就需要一个协调者作为中间人,这个中间人统一掌握所有服务,协调者知道所有节点的执行结果并最终确认并指示相应的节点是否执行最后提交。
而两阶段提交就是说将整个事务处理的过程分为两个阶段,分别是准备阶段和提交阶段
这是基于XA协议实现的分布式事务,XA协议中分成了两个部分:事务管理器和本地资源管理器。
本地资源管理器由数据库实现,比如MySQL都实现了XA接口,而事务管理器则作为一个全局的调度者。
两阶段提交(2PC
),对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。
两阶段提交的第一阶段,协调者向所有参与者发送请求,询问它们是否可以进行事务提交
第二阶段是提交(Commit)阶段。在该阶段,协调者根据第一阶段的反馈结果,决定是否进行事务的提交。
可见,这是一个强一致性的同步阻塞协议,事务执行过程中需要把所需资源全部锁住,任何其他事务不可操作这些资源,所以只适合执行时间比较短的事务,整体性能比较差。
并且一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。
三阶段提交型(3PC)是二阶段提交型(3PC)的一种改进版本,是为了解决2PC出现的提交协议的阻塞问题。
2PC最大的问题就是过度依赖事务协调者,一旦事务协调者出现问题,那么参与者就无法做出最后的选择,随后导致阻塞资源锁定资源。
在2PC中事务协调者存在超时机制,但是参与者并不存在超时机制:
于是,在3PC中事务协调者和参与者都引入了超时机制,并且在第一第二阶段又插入了一个准备阶段,当事务协调者出现故障时,参与者就不会一直阻塞,保证了在最后提交阶段之前各参与节点的状态是一致的。
三阶段提交的过程如下:
TCC是指Try
、Confirm
、Cancel
三个操作
Try
:预留业务资源;Confirm
:确认执行业务操作;Cancel
:取消执行业务操作。该方案的大体步骤如下:
Try
先锁住对象资源进行预留操作,只要预留资源这一步成功了,才会有后面的操作;Confirm
操作,也就是对 Try
阶段锁定的资源进行业务操作;Confirm
执行失败,则执行 Cancel
操作,也就是在所有操作失败时的回滚操作,释放预留资源。对于TCC补偿型来说,不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则通过Cancel
阶段进行回滚补偿,也就是补偿性事务。
但是TCC的代码侵入性非常强,因为原本一个方法但如今需要三个方法来完成。并且这种模式的代码不能很好被复用,会导致开发量增加。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
最终一致性型,也成为消息事务,这是因为这种方案是基于MQ实现的,将本地事务和发消息放在同一个事务里,保证本地操作和发消息同时成功。
MQ
发送一条预备扣减库存消息,MQ
保存预备消息并返回成功 ACK
ACK
,订单系统执行本地下单操作,为防止消息发送成功而本地事务失败,订单系统会实现 MQ
的回调接口,其内不断的检查本地事务是否执行成功,如果失败则 rollback
回滚预备消息;成功则对消息进行最终 commit
提交。基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况。
最大努力通知型事务(Best-Effort Delivery Transaction)是一种分布式系统中常见的事务模型。在这种模型下,系统不保证对所有参与者的通知操作一定会成功到达,而是采取尽最大努力去通知参与者。
最大努力通知型事务的特点如下:
最大努力通知型事务常见于异步通信场景,其中通知操作的可靠性要求相对较低,可以容忍一定的失败率。例如,使用消息队列进行异步通知,发布者将通知消息发送给订阅者,但无法保证每个订阅者都能够成功接收到通知。
在设计最大努力通知型事务时,需要考虑以下几点:
最大努力通知型事务适用于那些对实时性要求不高,而且可以容忍一定通知失败率的业务场景。对于一些对可靠性和一致性要求更高的场景,可能需要选择其他更强大的事务模型或协议来保证数据的一致性和可靠性。
支付回调就是类似的原理,支付接口都需要一个回调地址,在支付成功后,支付接口提供方会将支付结果返回到我们的回调地址,如果没有收到支付成功的通知,支付接口提供方会重复调用我们的接口,直到通知指定次数后不再通知。
Seata
是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。官网:Seata | Seata
Seata
也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT
、TCC
、SAGA
和 XA
等事务模式。
Seata
分布式事务的几个角色:
Transaction Coordinator(TC)
: 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交。
Transaction Manager™
: 事务管理者,业务层中用来开启/提交/回滚一个整体事务(在调用服务的方法中用注解开启事务)。
Resource Manager(RM)
: 资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction
),理分支事务与 TC
进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。
Seata
实现分布式事务,设计了一个关键角色UNDO_LOG
(回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在UNDO_LOG
表中,以便业务异常能随时回滚。
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模式要好很多
Seata
对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM一阶段的工作:
① 注册分支事务到TC
② 执行分支业务sql但不提交
③ 报告执行状态到TC
TC二阶段的工作:
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务
RM二阶段的工作:
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try
:资源的检测和预留;
Confirm
:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel
:预留资源释放,可以理解为try的反向操作。
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
初识余额:
余额充足,可以冻结:
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
需要回滚,那么就要释放冻结金额,恢复可用金额:
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
首先,我们需要下载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"
}
}
在服务中引入Seata
依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-spring-boot-starterartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>${seata.version}version>
dependency>
在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
只需要使用@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