1、Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,Seata 将为用户提供 AT、TCC、SAGA 等事务模式,Seata为用户打造一站式的分布式解决方案。
2、Seata 设计目标
Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
3、Seata组成
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:
Transaction Coordinator ( TC ): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager( TM ): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager ( RM ): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:
下面是一个分布式事务在Seata中的执行流程:
1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2. XID 在微服务调用链路的上下文中传播。
3. RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
4. TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
4、Seata 模式
(1)AT模式(默认模式):
AT模式是一种没有侵入的分布式事务的解决方案,在AT模式下,用户只需关注自己的业务SQL,用户的业务SQL作为第一阶段,Seata框架会自动生成事务进行第二阶段提交和回滚操作。
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
在一阶段中,Seata会拦截业务SQL ,首先解析SQL语义,找到要操作的业务数据,在数据被操作前,保存下来记录 undo log,然后执行 业务SQL 更新数据,更新之后再次保存数据 redo log,最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。
相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的 undo Log ,通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。
AT模式主要特点:
1. 最终一致性
2. 性能较XA高
3. 只在第一阶段获取锁,在第一阶段进行提交后释放锁。
seata写隔离 机制 :
· 一阶段本地事务提交前,需要确保先拿到全局锁。
· 拿不到全局锁,不能提交本地事务。
· 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明【官网】:两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。tx1 先开始,开启本地事务,①、拿到本地锁,②、更新操作 m = 1000 - 100 = 900。本地事务提交前,③、先拿到该记录的 全局锁 ,④、本地提交并且释放本地锁。tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放全局锁。tx2 拿到全局锁提交本地事务。
如果 tx1 二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。因为整个过程全局锁在tx1结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
seata读隔离 机制:****
在数据库本地事务隔离级别为读已提交(Read Committed)或以上的基础上,Seata(AT 模式)的默认全局隔离级别是读未提交(Read Uncommitted)。
如果应用在特定场景下,必须要求全局的读已提交,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并CAS自旋重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
(2)TCC模式:
TCC方案其实是两阶段提交的一种改进。分成了Try、Confirm、Cancel三个操作。事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚;
TCC 模式,不依赖于底层数据资源的事务支持:
· 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
· 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
· 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。简单点概括,SEATA的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑而不依赖AT模式的undo_log。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
(3)SAGA模式:
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
适用场景:
业务流程长、业务流程多,参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。
优势:
一阶段提交本地事务,无锁,高性能;事件驱动架构,参与者可异步执行,高吞吐;补偿服务易于实现。
缺点:
Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。
三种模式比较分析:****
· AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
· TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
· Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式第一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。
5、Seata环境安装****
a、Seata部署
生产部署建议采用高可用方式,其部署架构为Seata-Server部署3个节点,一并注册到Nacos中心;(本地演示环境用的是单点的Nacos以及Seata)
b、Seata配置
1)、registry.conf配置修改:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "default"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
2)、file.conf配置修改:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#test_tx_group 与 yml 中的 “tx-service-group: test_tx_group” 配置一致
vgroup_mapping.test_tx_group = "default" #修改自定义事务组名称
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
disableGlobalTransaction = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
report.retry.count = 5
tm.commit.retry.count = 1
tm.rollback.retry.count = 1
}
store {
## store mode: file、db
mode = "db" #修改此处将事务信息存储到数据库中
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://47.111.178.164:3306/seata"
user = "seata"
password = "123456"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
lock {
## the lock store mode: local、remote
mode = "remote"
local {
## store locks in user's database
}
remote {
## store locks in the seata's server
}
}
recovery {
#schedule committing retry period in milliseconds
committing-retry-period = 1000
#schedule asyn committing retry period in milliseconds
asyn-committing-retry-period = 1000
#schedule rollbacking retry period in milliseconds
rollbacking-retry-period = 1000
#schedule timeout retry period in milliseconds
timeout-retry-period = 1000
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
undo.log.save.days = 7
#schedule delete expired undo_log in milliseconds
undo.log.delete.period = 86400000
undo.log.table = "undo_log"
}
metrics {
enabled = false
registry-type = "compact"
# multi exporters use comma divided
exporter-list = "prometheus"
exporter-prometheus-port = 9898
}
support {
## spring
spring {
# auto proxy the DataSource bean
datasource.autoproxy = false
}
}
3)、Nacos增加Seata配置
建议用Git Bash命令来上传Seata配置,具体操作如下:
(1)进入Seata的conf文件夹;
(2)右键执行命令Git Bash Here ,打开弹框;
(3)执行命令;
sh nacos-config.sh localhost
注意:localhost 替换为nacos的ip地址,还可以添加其他的参数,比如:-p 指定nacos的端口地址;-g 指定配置的分组,注意,是配置的分组;-t 指定命名空间id; -u -w指定nacos的用户名和密码
(4)上传后可以看到如下效果;
c、创建Seata依赖的数据库表:
drop database if exists seata;
CREATE DATABASE seata CHARSET utf8;
use seata;
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS global_table
(
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
status TINYINT NOT NULL,
application_id VARCHAR(32),
transaction_service_group VARCHAR(32),
transaction_name VARCHAR(128),
timeout INT,
begin_time BIGINT,
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (xid),
KEY idx_gmt_modified_status (gmt_modified, status),
KEY idx_transaction_id (transaction_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS branch_table
(
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
resource_group_id VARCHAR(32),
resource_id VARCHAR(256),
branch_type VARCHAR(8),
status TINYINT,
client_id VARCHAR(64),
application_data VARCHAR(2000),
gmt_create DATETIME(6),
gmt_modified DATETIME(6),
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS lock_table
(
row_key VARCHAR(128) NOT NULL,
xid VARCHAR(96),
transaction_id BIGINT,
branch_id BIGINT NOT NULL,
resource_id VARCHAR(256),
table_name VARCHAR(32),
pk VARCHAR(36),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (row_key),
KEY idx_branch_id (branch_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
d、服务启动
nohup sh seata-server.sh -p port -h ip >seata.log 2>&1 &
e、验证服务
6、 应用接入:
1)业务库中创建undo_log表:
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS undo_log
(
branch_id BIGINT(20) NOT NULL COMMENT 'branch transaction id',
xid VARCHAR(100) NOT NULL COMMENT 'global transaction id',
context VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
rollback_info LONGBLOB NOT NULL COMMENT 'rollback info',
log_status INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
log_created DATETIME(6) NOT NULL COMMENT 'create datetime',
log_modified DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
2)、bootstrap.yml配置
3)、将Seata中的配置文件file.conf以及registry.conf复制到resource目录下
4)、添加项目的POM依赖
5)、代码接入
应用服务层,链路执行开始方法签名上增加开启全局事务标记及回滚异常
@GlobalTransactional(rollbackFor = Exception.class)
非链路开始方法或其他被调用服务,方法前面上增加开启本地事务标记,同时方法必须抛出异常(被上述全局事务回滚异常包含)
@Transactional
6)回滚日志:
[demo:192.168.24.119:0000] 2022-12-19 14:27:18.259 INFO 22780 [] [http-nio-8088-exec-3] i.seata.tm.api.DefaultGlobalTransaction Begin new global transaction [192.168.24.119:8091:348110615241781248]
-------------------执行业务代码-----------------
[demo:192.168.24.119:0000] 2022-12-19 14:27:18.947 INFO 22780 [] [rpcDispatch_RMROLE_1_2_16] i.s.c.r.p.c.RmBranchRollbackProcessor rm handle branch rollback process:xid=192.168.24.119:8091:348110615241781248,branchId=348110616177111040,branchType=AT,resourceId=jdbc:mysql://47.111.178.164:3306/labor_dev,applicationData=null
[demo:192.168.24.119:0000] 2022-12-19 14:27:18.947 INFO 22780 [] [rpcDispatch_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler Branch Rollbacking: 192.168.24.119:8091:348110615241781248 348110616177111040 jdbc:mysql://47.111.178.164:3306/labor_dev
[demo:192.168.24.119:0000] 2022-12-19 14:27:19.029 INFO 22780 [] [rpcDispatch_RMROLE_1_2_16] i.s.r.d.undo.AbstractUndoLogManager xid 192.168.24.119:8091:348110615241781248 branch 348110616177111040, undo_log deleted with GlobalFinished
[demo:192.168.24.119:0000] 2022-12-19 14:27:19.043 INFO 22780 [] [rpcDispatch_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler Branch Rollbacked result: PhaseTwo_Rollbacked
[demo:192.168.24.119:0000] 2022-12-19 14:27:19.051 INFO 22780 [] [http-nio-8088-exec-3] i.seata.tm.api.DefaultGlobalTransaction [192.168.24.119:8091:348110615241781248] rollback status: Rollbacked
[demo:192.168.24.119:0000] 2022-12-19 14:27:19.053 ERROR 22780 [] [http-nio-8088-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
[demo2:192.168.24.119:0000] 2022-12-19 10:51:23.314 INFO 22036 [] [rpcDispatch_RMROLE_1_3_16] i.s.c.r.p.c.RmBranchRollbackProcessor rm handle branch rollback process:xid=192.168.24.119:8091:348056277194600448,branchId=348056277966352384,branchType=AT,resourceId=jdbc:mysql://47.111.178.164:3306/labor_dev,applicationData=null
[demo2:192.168.24.119:0000] 2022-12-19 10:51:23.314 INFO 22036 [] [rpcDispatch_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler Branch Rollbacking: 192.168.24.119:8091:348056277194600448 348056277966352384 jdbc:mysql://47.111.178.164:3306/labor_dev
[demo2:192.168.24.119:0000] 2022-12-19 10:51:23.428 INFO 22036 [] [rpcDispatch_RMROLE_1_3_16] i.s.r.d.undo.AbstractUndoLogManager xid 192.168.24.119:8091:348056277194600448 branch 348056277966352384, undo_log deleted with GlobalFinished
[demo2:192.168.24.119:0000] 2022-12-19 10:51:23.445 INFO 22036 [] [rpcDispatch_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler Branch Rollbacked result: PhaseTwo_Rollbacked
从控制台打印出来的内容可以发现,如果程序出现异常,之前已经执行的所有分支事务都会发生回滚