Seata
学习seata之前,我们先要回顾一下事务和了解一下分布式事务相关的知识,有助于我们理解seata。seata service搭建完成后,使用起来是很简单的。
事务
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据 库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。 这四个属性通常称为ACID特性。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,即使不能都很好的满足,也要考虑支持到什么程度。
-
原子性(Atomicity):事务是一个不可分割的工作单位,事务中包括的诸多操作要么都做,要么都不做。
-
一致性(Consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
-
隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
隔离性又分为四个级别:- 读未提交(read uncommitted)
- 读已提交(read committed,解决脏读)
- 可重复读(repeatable read,解决虚读)
- 串行化(serializable,解决幻读)
-
持久性(Durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
分布式事务
完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成 功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
典型的分布式事务场景举例:
-
跨库事务
一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据 。 -
分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分 ,也就是分库分表的情况。
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对 于sql:insert into user(id,name) values (1,“张三”),(2,“李四”)。这条sql是操作单库的语法,单库情 况下,可以保证事务的一致性。但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。 -
服务化
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败。实际上这可能是最典型的分布式事务场景。
常见分布式事务解决方案
解决分布式事务,也有相对应的规范和协议。分布式事务相关的协议有2PC、3PC。由于三阶段提交协议3PC非常难实现,目前市面上主流的分布式事务解决方案都是基于2PC。
- seata
- 消息队列
- saga
- XA
他们有一个共同点,都是两阶段提交协议(2PC)。两阶段是指完成整个分布式事务划分成两个步骤完成。上面常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、Saga、XA。 四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出。每种模式都有它的适用场景,同样每个模式也都诞生有各自的代表产品。而这些代表产品,可能就是我们常见的(全局事务、 基于可靠消息、最大努力通知、TCC)。
2PC两阶段提交协议
2PC(Two-Phase Commit)两阶段提交协议,顾名思义,分为两个阶段:Prepare和Commit
Prepare:提交事务请求
大概流程如下:
- 询问——协调者向所有参与者发送事务请求,询问是否可以执行事务操作,然后等待所有参与者的响应。
- 执行——所有参与者接受到协调者事务请求后,所有参与者执行事务操作(例如更新插入等操作),并将Undo和Redo信息记录在事务日志中。
- 响应——如果所有参与者成功的执行了事务并写入了Undo和Redo信息,则向协调者返回YES,否则返回NO。此时参与者如果宕机,就不会有任何响应给协调者。
Commit:执行事务提交
执行事务提交分为两种情况,正常提交和中断。
- 正常提交事务
- commit——协调者向所有参与者发送Commit请求。
- 事务提交——参与者收到Commit请求后,执行事务提交,提交完成后释放事务执行期间占有的所有资源。
- 反馈结果——参与者执行事务提交后向协调者发送ACK响应。
- 完成事务——接收到所有参与者的ACK响应后,完成事务提交。
- 中断事务
在执行Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无法 收到所有参与者的YES响应,或者某个参与者返回了 No 响应,此时,协调者就会进入回退流程,对事务进行回 退。流程如下图红色部分(将 Commit 请求替换为红色的 Rollback 请求):
- Rollback——协调者向所有参与者发送 Rollback 请求。
- 事务回滚——参与者收到 Rollback 后,使用 Prepare 阶段的Undo日志执行事务回滚,完成后释放事务执行 期占用的所有资源。
- 反馈结果——参与者执行事务回滚后向协调者发送 Ack 响应。
- 中断事务——接收到所有参与者的ACK响应后,完成事务中断。
2PC的弊端:
- 同步阻塞,参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进 行其他操作的,也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信 息,那么会导致参与者一直阻塞下去。
- 单点在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。 如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务。
- 数据不一致,Commit事务过程中Commit请求/Rollback 请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到Commit/Rollback请求,而其他参与者则正常收到执行了Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。 当参与者执行Commit/Rollback后会向协调者发送ACK,然而协调者不论是否收到所有的参与者的ACK,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成 功”。
- 环境可靠性依赖协调者Prepare请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后, 会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
AT、TCC、Saga、XA四种事务模式
- AT模式(Auto Transcation)
AT模式是一种无侵入的分布式事务解决方案。阿里的seata框架实现了该模式。在AT模式下,用户只需要关注自己的业务SQL,用户的业务SQL作为一阶段,seata框架会自动生成事务的二阶段提交和回滚操作。
一阶段:
在一阶段,Seata会拦截“业务 SQL”,首先解析SQL语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据, 在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交:
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以Seata框架只需将一 阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚:
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据。但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一 致就说明有脏写,出现脏写就需要转人工处理。
AT模式的一阶段、二阶段提交和回滚均由Seata框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
-
TCC模式(Try Commit Cancel)
TCC模式入侵性比较强,并且得自己实现相关事务控制逻辑。但是,在整个过程中基本没有锁,所以性能更强。常见的TCC模式框架:BeyeTCC、TCC-transction、Himly
TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作。事务发起方在一阶段执行Try方式,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。
TCC 三个方法描述:
-
Try:资源的检测和预留
-
Confirm:执行的业务操作提交。要求Try成功Confirm一定要能成功
-
Cancel:预留资源释放
-
-
Saga模式
补偿协议:业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。一阶段正向服务和二阶段补偿服务都由业务开发实现。
saga模式的实现,是长事务解决方案。saga是一种补偿协议,在saga模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
如图:T1-Tn都是正向业务流程,都对应着一个冲正逆向操作C1-C3
逻辑如下:分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。 Saga 正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的。 Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
saga模式适用场景
Saga模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。 事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供TCC要求的接口,可以使用 Saga 模式。saga模式的优缺点
优点:
- 一阶段提交本地数据库事务,无锁,高性能
- 参与者可以采用事务驱动异步执行,高吞吐
- 补偿服务即正向服务的“反向”,易于理解,易于实现
缺点:
Saga模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。
-
XA模式
在XA模式下,需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协调器,把结果给协调器。协调器等所有分支事务操作完成、都预提交后,进行第二步。第二步协调器通知每个数据库进行逐个commit/rollback。其中,这个全局协调器就是XA模型中的TM角色,每个分支事务各自的数据库就是RM。XA模式缺点:事务粒度大。高并发下,系统可用性低。因此很少使用。
AT、TCC、Saga、XA 四种模式总结
四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景。
-
AT模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学 习成本。
-
TCC模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
-
Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统, Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供TCC要求的接口,也可以使用 Saga 模式。
-
XA模式是分布式强一致性的解决方案,但性能低而使用较少。
分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择。但是,我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作(代码量上 去了,业务复杂了,性能下跌了)。 所以,当我们真实开发的过程中,能不使用分布式事务就不使用。
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了 AT、TCC、Saga 和 XA 事务模式,AT模式是阿里首推的模式 。
相关官方文档:
官网:https://seata.io/zh-cn/index.html
源码: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples
Seata相关设计
在 Seata 的架构中,一共有三个角色。其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的 Client 客户端。
-
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。 -
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。 -
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
在 Seata 中,一个分布式事务的生命周期如下:
-
TM请求TC开启一个全局事务。TC 会生成一个XID作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。 当一进入事务方法中就会生成XID。global_table表中就是存储的全局事务信息
-
RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联。当运行数据库操作方法,branch_table表中存储事务参与者信息
-
TM请求TC告诉XID对应的全局事务是进行提交还是回滚。
-
TC驱动RM们将XID对应的自己的本地事务进行提交还是回滚。
设计思路
AT模式的核心是对业务无入侵,是一种改进后的二阶段提交。设计思路如下:
第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库。
第二阶段
分布式事务操作成功,则TC通知RM异步删除undolog
分布式事务操作失败,TM向TC发送回滚请求,RM收到协调器TC发来的回滚请求,通过XID和Branch ID找到相应的回滚日志记 录,通过回滚记录生成反向的更新SQL并执行(执行前会将before image和after image进行对比),然后删除事务占用的资源。完成分支的回滚。
设计亮点
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性。
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚。
- 通过全局锁实现了写隔离与读隔离。
存在的问题
-
性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一 次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
-
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
-
热点数据
相比XA,Seata虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。
-
回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
-
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
Seata使用
使用Seata需要单独部署Seata的服务,配置中心等。在服务中也需要创建相关表。相关构建可能会有点复杂,但是用是需要使用一个@GlobalTransaction注解就好。
这次是在本地win上搭建,在linux上搭建需要修改的配置文件和win差不多。后期有机会再在linux上部署一下。
Seata Server(TC)搭建
Seata服务端下载地址:
https://github.com/seata/seata/releases
结合自己项目的spring cloud alibaba版本选择合适的Seata版本。我这边使用的是seata-server-1.3.0
下载完成后,解压即可。接下来就是修改配置了。
修改Server端存储模式(store.mode)
Server端支持三种存储模式:
- file:默认模式,单机模式。全局事务会话信息存在内存中,读写并持久化到本地文件root.data中。性能较高。
- db:要求mysql5.7+,高可用模式。全局事务会话信息通过db共享,性能稍差一些。
- redis:Seata-Server1.3及以上斑斑支持,性能较高,但是存在事务信息丢失的风险。建议提前配置适当的当前场景的redis持久化配置。
这边使用db模式演示,具体操作如下:
-
修改seata\conf\file.conf文件
-
创建数据库
根据上一步中的配置文件中的配置,建立对应的数据库
-
创建表
数据库脚本可以在官方文档——>运维指南——>部署——>新人文档——>资源目录介绍,点击查看处获取。注意这里部署的是server端。
https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql
Seata整合nacos
-
整合注册中心
说明:这两项配置可以更改负载均衡模式
loadBalance = “RandomLoadBalance”
loadBalanceVirtualNode = 10
loadBalanceVirtualNode = 10
- 整合配置中心
注意:如果seata-server使用了nacos作为配置中心,配置信息就会从nacos中读取。配置registry.conf使用nacos时,group配置要和seata-server中的group一样。
-
修改seata配置中心信息
先从seata官网(https://github.com/seata/seata/tree/1.4.0)获取seata源码工程的压缩文件,然后获取seata-1.4.0\script\config-center\config.txt并修改部分配置
seata-1.4.0\script\config-center\config.txt文件中的一些说明:
service.vgroupMapping.my_test_tx_group=default
-
使用了seata的客户端需要和my_test_tx_group保存一致。my_test_tx_group也可以自定义为guangzhou、shanghai等这样根据服务器地域进行命名(本次配置中使用的是"service.vgroupMapping.guangzhou=default"),可以用作异地灾备的一个配置。那么对应的客户端就要如下配置
spring.cloud.alibaba.seata.tx_service_group=guangzhou
-
default需要等于registry.confi中registry配置中的cluster对应的值
-
同步配置参数到nacos
在seata-1.4.0\script\config-center\nacos下运行以下脚本
sh nacos-config.sh -h 192.168.2.7 -p 8848 -g SEATA_GROUP -t 8375c8f1-32d7-4d34-80f0-4ff80378c437 -u nacos -w nacos # 相关参数说明: # -h nacos主机host,默认值localhost # -p nacos服务port,默认值8848 # -g 配置的分组,默认值SEATA_GROUP # -t 租户信息,对应nacos中的namespaceID 默认为nacos的public # -u nacos登录名,需要有读写权限 # -w 对应登录名的密码
然后就可以在nacos配置中心看到这些配置。
- 启动seata-server服务
seata/bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
参数说明:
参数 | 全写 | 作用 | 备注 |
---|---|---|---|
-h | –host | 指定在注册中心中注册的IP | 不指定时获取当前的IP,外部访问部署在云环境和容器中时建议指定 |
-p | –port | 指定server启动的端口 | 默认为8091 |
-m | –storeMode | 事务日志存储方式 | 支持file,db,redis(需要seata-server1.3+)。默认为file |
-n | –serverNode | 用于指定seata-server节点ID | 如1,2,3,…默认为1 |
-e | –seataEvn | 指定seate-server运行环境 | 如dev,test等,服务启动时会使用registry-dev.conf这样的配置 |
集群部署:
seata集群的方式在win下不是很好部署。在linux下部署很方便,因为它不像nacos一样还需使用nginx来负载,seata直接使用nacos的LB。
# 机器1
seata/bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
# 机器2
seata/bin/seata-server.sh -h 127.0.0.1 -p 8092 -m db -n 2 -e test
# 机器3
seata/bin/seata-server.sh -h 127.0.0.1 -p 8093 -m db -n 3 -e test
双击seata\bin下seata-server.bat启动Seata server。
在启动seata server服务时遇到了一个Could not create connection to database server报错,已经解决了,可移步查看
使用seata
下面的操作都是基于上一步的部署成功的seata server服务中进行。
-
添加依赖
在使用到分布式事务的服务pom文件下添加seata依赖。<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
创建undo_log表
在使用到分布式事务的服务对应的数据库中添加undo_log表,脚本如下:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
添加配置文件
在使用到分布式事务的服务配置文件中添加seata相关配置。# 数据源 spring: datasource: username: root password: password url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC& driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #初始化时运行sql脚本 schema: classpath:sql/schema.sql initialization-mode: never application: name: alibaba-order-seata cloud: nacos: discovery: server-addr: 192.168.2.7:8848 username: nacos password: nacos alibaba: seata: tx-service-group: guangzhou #配置事务分组 看下自己配置中心的事务分组名称是啥 #设置mybatis mybatis: mapper-locations: classpath:com/kiwi/order/mapper/*Mapper.xml #config-location: classpath:mybatis-config.xml typeAliasesPackage: com.kiwi.order.pojo configuration: mapUnderscoreToCamelCase: true server: port: 8072 seata: registry: # 配置seata的注册中心, 告诉seata client 怎么去访问seata server(TC) type: nacos nacos: server-addr: 192.168.2.7:8848 # seata server 所在的nacos服务地址 application: seata-server # seata server 的服务名seata-server ,如果没有修改可以不配 username: nacos password: nacos group: SEATA_GROUP # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配 namespace: 8375c8f1-32d7-4d34-80f0-4ff80378c437 # 自己seata注册中心namespace config: type: nacos nacos: server-addr: 192.168.2.7:8848 username: nacos password: nacos group: SEATA_GROUP namespace: 8375c8f1-32d7-4d34-80f0-4ff80378c437 # 自己seata配置中心namespace
-
使用@GlobalTransaction注解
在存在分布式事务的方法上添加@GlobalTransaction注解。@GlobalTransactional @Override public Order create(Order order) { // 插入能否成功? orderMapper.insert(order); // 扣减库存 能否成功? stockService.reduct(order.getProductId()); // 异常 int a=1/0; return order; }
启动服务,测试发生异常的回滚效果:
接口是访问不了
控制台报错
库存服务数据库数据未扣减,分布式事务生效。