一、介绍:
Seata是阿里巴巴旗下的产品,是一款开源的分布式事务解决方案,旨在解决分布式事务问题。
我们有必要先了解一下分布式事务:
在微服务体系中,每一个模块都有连接一个数据库,这一点与单体项目是不同的,单体项目就连接一个数据库。
那么如果有多个模块之间相互调用,怎样保证各个模块之间的事务一致性?由此引出了分布式事务。
在同一个数据库中,我们要保证事物的一致性是很简单的。因为MySQL是基于单机事物的,所以一旦遇到跨库的场景,那么MySQL数据库就无能为力了。在这种情景下,seata蕴育而生。
二、Seata的下载和启动:
seata的官网:Apache Seata
seata的下载地址:
https://github.com/apache/incubator-seata/releases/download/v2.0.0/seata-server-2.0.0.zip
有三个概念要先了解:
-
TC是事务协调器(就是Seata本身),负责管理全局事务的执行过程。它生成全局唯一的事务ID,并协调各个分支事务的提交或回滚,以确保数据的一致性。有且仅有一个
-
TM是事务管理器,(标注全局@GlobalTransactional启动入口动作的微服务模块)负责发起全局事务的提交或回滚请求,并与TC交互以执行相应操作。它与业务逻辑代码交互,触发分支事务的执行,并根据TC的指示来决定全局事务的最终状。有且仅有一个
-
.RM是资源管理器,(各个模块的MySQL数据库本身)负责管理分支事务的资源,如数据库、消息队列、缓存等。它接收TC的指令并执行相应的事务操作,以确保分支事务的一致性。可以有多个
总的来说,TC负责全局事务的协调和管理,TM负责全局事务的发起和控制,RM负责具体资源的管理和事务操作。它们共同协作,实现了Seata分布式事务框架的核心功能,确保分布式系统中事务的一致性和可靠性。
下载完seata之后,要使用seata要现在本地的MySQL数据库中新建一个数据库seata,来记录seata在运行过程中的信息;
CREATE DATABASE seata;
USE seata;
sql脚本:
create table branch_table
(branch_id bigint not nullprimary key,xid varchar(128) not null,transaction_id bigint null,resource_group_id varchar(32) null,resource_id varchar(256) null,branch_type varchar(8) null,status tinyint null,client_id varchar(64) null,application_data varchar(2000) null,gmt_create datetime(6) null,gmt_modified datetime(6) null
);create index idx_xidon branch_table (xid);create table distributed_lock
(lock_key char(20) not nullprimary key,lock_value varchar(20) not null,expire bigint null
);INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO seata.distributed_lock (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);create table global_table
(xid varchar(128) not nullprimary key,transaction_id bigint null,status tinyint not null,application_id varchar(32) null,transaction_service_group varchar(32) null,transaction_name varchar(128) null,timeout int null,begin_time bigint null,application_data varchar(2000) null,gmt_create datetime null,gmt_modified datetime null
);create index idx_status_gmt_modifiedon global_table (status, gmt_modified);create index idx_transaction_idon global_table (transaction_id);create table lock_table
(row_key varchar(128) not nullprimary key,xid varchar(128) null,transaction_id bigint null,branch_id bigint not null,resource_id varchar(256) null,table_name varchar(32) null,pk varchar(36) null,status tinyint default 0 not null comment '0:locked ,1:rollbacking',gmt_create datetime null,gmt_modified datetime null
);create index idx_branch_idon lock_table (branch_id);create index idx_statuson lock_table (status);create index idx_xidon lock_table (xid);
创建好库和表之后,更改seata的内容来启动seata:
在seata的conf文件夹下,更改application.yml文件,启动seata:
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seata
seata:
# 配置方式nacosconfig:type: nacos# support: nacos, consul, apollo, zk, etcd3nacos:server-addr: localhost:8848namespace:group: SEATA_GROUPusername: nacospassword: nacoscontext-path:##if use MSE Nacos with auth, mutex with username/password attribute#access-key:#secret-key:data-id: seataServer.properties
# 注册方式 nacosregistry:# support: nacos, eureka, redis, zk, consul, etcd3, sofatype: nacos# preferred-networks: 30.240.*nacos:application: seata-serverserver-addr: localhost:8848group: SEATA_GROUPnamespace:cluster: defaultusername: nacospassword: nacos
# 存储方式 db mysqlstore:mode: db# support: file 、 db 、 redis 、 raftdb:datasource: druiddb-type: mysqldriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueuser: rootpassword: 123456min-conn: 10max-conn: 100global-table: global_tablebranch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000# server:# service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
根据你nacos、MySQL表的信息,进行相应的更改;
先启动nacos、再启动seata;(seata的启动在bin下的seata-server.bat)
可以看到seata已经作为一个模块注册进了nacos中;
访问7091端口,可以看到seata的图形化界面(用户名和密码都是seata)
三、Seata测试Demo(AT模式)
业务说明:订单服务→库存服务→支付服务:下订单 --->减库存---->扣余额---->修改(订单)状态
maven依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
配置文件application(seata相关):
seata:registry:type: nacosnacos:server-addr: localhost:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group #事务组,由他获得TC服务的集群名称service:vgroup-mapping: #源码默认名字 default_tx_group defaultdefault_tx_group: default #事务组与TC服务集群的映射关系data-source-proxy-mode: AT #默认是AT 如果是AT可不写logging:level:io:seata: info
主要业务方法:
其中int i =10/0;用于测试异常情况
@Override@GlobalTransactional(name = "create-order-transaction", rollbackFor = Exception.class)public void create(Order order) {// xid全局事务检查String xid = RootContext.getXID();// 1. 新建订单log.info("-------------> 开始新建订单, XID: {}", xid);order.setStatus(0);int result = orderMapper.insertSelective(order);Order orderFromDB;if (result > 0) {orderFromDB = orderMapper.selectOne(order);log.info("-------------> 新建订单成功, OrderInfo: {}", orderFromDB);// 2. 扣减库存log.info("-------------> 开始扣减库存");storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());log.info("-------------> 扣减库存成功");// 3. 扣减账户余额log.info("-------------> 开始扣减余额");accountFeignApi.decrease(order.getUserId(), order.getMoney());log.info("-------------> 扣余额存成功");int i = 10/0;// 4. 修改订单状态log.info("-------------> 开始修改订单状态");Example whereCondition = new Example(Order.class);Example.Criteria criteria = whereCondition.createCriteria();criteria.andEqualTo("id", orderFromDB.getId());criteria.andEqualTo("status", 0);orderFromDB.setStatus(1);int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);log.info("-------------> 修改订单状态成功");}log.info("-------------> 结束新建订单, XID: {}", xid);}
调用接口前三个数据库,三张表以及对应数据库undo_log表如下:
执行过程中(断点在异常语句处)数据库及seata控制台:
执行完成:
结果:可以看到在过程中记录添加进数据库并进行了相关记录,异常后进行了回滚。
四、总结:
我们之前的步骤都是建立在seata的AT模式上;
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
在一阶段,Seata 会拦截“业务 SQL”,
1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
2 执行“业务 SQL”更新业务数据,在业务数据更新之后,
3 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段分为两种情况:
1、正常提交:
二阶段如是顺利提交的话,
因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
2、异常回滚:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。