文章首发微信公众号:【Coder-文小白】,欢迎大家进行技术交流。
一、seata框架简介
Seata
(Simple Extensible Autonomous Transaction Architecture
)是一个开源的分布式事务解决方案,旨在解决微服务架构下的事务一致性问题。它提供了高性能、易扩展的分布式事务服务,帮助开发者在分布式系统中实现事务管理。Seata 2.0
版本是这一框架的一个重要更新,带来了更多的功能和优化。例如,Seata 2.0
版本继续优化了事务处理的性能,通过更高效的事务日志存储方式和更精细的并发控制策略,提高了事务处理的吞吐量和响应速度。
更多关于seata2.0
版本新特性,可以通过官网(https://seata.apache.org/zh-cn/)进行了解。
二、安装准备
本次我们安装过程中使用nacos
作为注册中心和配置中心,所以需要部署好nacos
环境;同时seata
框架需要依赖于数据库,需要准备好数据库环境,数据库环境的搭建比较简单,只需要运行seata
的初始化SQL
脚本,进行数据库连接即可,数据库的安装不再赘述,可以自行安装。
1. nacos部署
介质下载地址:https://download.nacos.io/nacos-server/nacos-server-2.4.0.1.zip
下载完成以后,进行安装包解压,解压完成以后,目录参考如下:
进入bin目录,通过单机方式启动nacos
:
startup.cmd -m standalone
启动完成以后,通过浏览器访问 http://localhost:8848/nacos 地址,能进入nacos
管控台则说明启动成功。
2. seata部署
介质下载地址:https://github.com/apache/incubator-seata/releases/download/v2.0.0/seata-server-2.0.0.zip
下载完成以后,进行安装包解压,解压完成以后,目录参考如下:
进入到script\server\db
目录下,根据自身的数据库环境选择不同的SQL
初始化脚本,进行数据库初始化:
我本地环境是mysql数据库,首先创建一个数据库:
CREATE DATABASE `seata` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
然后执行mysql.sql
里面的SQL
脚本:
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- 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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- 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 = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
nacso
和数据库环境准备好以后,接下来就是修改seata
相关配置了,重新进入conf目录,找到application.yml
文件,可以参考目录下application.example.yml
文件进行配置修改,我本地环境是使用nacos
作为注册中心和配置中心,数据库环境是mysql 8.0
,修改以后参考配置如下:
# 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: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: E:\\00-distribute-tx\\seata\\logs # 修改日志存储路径
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata # 管控端登录用户名
password: seata # 管控端登录密码
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # 本地nacos作为配置中心
namespace: public
group: SEATA_GROUP
username:
password:
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
data-id: seataServer.properties # nacos配置中心上的信息
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos: # 本地nacos作为注册中心
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
cluster: default
username:
password:
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
store:
# support: file 、 db 、 redis 、 raft
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver # 数据库连接信息
url: jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
user: root
password: root
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
刚刚我们使用了nacos
作为配置中心,因此我们需要到nacos
上配置相关属性信息,访问 http://localhost:8848/nacos/ 页面,在配置列表
菜单点击创建配置
:
注意Data ID
和Group
属性要和配置文件中保持一致,配置内容参考如下:
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
## mysql 5.xx
## driverClassName = "com.mysql.jdbc.Driver"
## mysql 8.0
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 对应导入的数据库
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&useSSL=false
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.distributedLockTable=distributed_lock
# 对应 ${spring.application.name}-group,指定需要分布式事务的模块
service.vgroupMapping.seata-validate-demo-group=default
service.vgroupMapping.default=default
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
完成以上配置以后,重新进入lib目录下,里面有一个jdbc目录,进入以后,根据需要使用的mysql
数据库驱动版本,拷贝相应的jar
包到lib目录下。
完成以上操作以后,重新进入bin目录,启动seata
:
seata-server.bat
启动完成以后,浏览器访问 http://localhost:7091 地址,如果出现登录页面,输入seata/seata 以后登录成功,则seata
部署完成。
三、Spring cloud集成seata2.0 AT模式实战
在部署好seata 2.0
以后,下面进行spring cloud
集成seata2.0 AT
模式实战环节。
首先,进行数据库环境准备,seata
AT
模式需要客户端应用创建undo_log
表,建表语句如下:
-- seata_demo.undo_log definition
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
创建好seata
框架需要的表以后,我们新建一个业务表,参考SQL
如下:
-- seata_demo.account definition
CREATE TABLE `account` (
`name` varchar(100) NOT NULL,
`account` decimal(10,0) DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO seata_demo.account(name, account) VALUES('A', 100);
INSERT INTO seata_demo.account(name, account) VALUES('B', 100);
数据库环境准备好以后,创建一个spring boot
项目,我这里使用的spring boot
版本是2.7.18
,导入相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.5.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
编写AccountDO
类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccountDO {
private String name;
private double account;
}
编写Mapper
层方法:
import com.seata.demo.vo.AccountDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AccountMapper {
@Insert("INSERT INTO account\n" +
"(name, account)" +
"VALUES(#{name}, #{account});")
int insert(AccountDO accountVo);
@Update("UPDATE account " +
"SET account=account+#{account}" +
"WHERE name=#{name};")
int add (AccountDO accountVo);
}
编写Service
层类:
import com.seata.demo.mapper.AccountMapper;
import com.seata.demo.vo.AccountDO;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@GlobalTransactional
public int save(AccountDO accountVo) {
return accountMapper.insert(accountVo);
}
public int add(AccountDO accountVo) {
return accountMapper.add(accountVo);
}
}
编写controller
层类:
import com.seata.demo.service.AccountService;
import com.seata.demo.vo.AccountDO;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@RequestMapping("/save")
public int save(@RequestBody AccountDO accountVo) {
return accountService.save(accountVo);
}
@RequestMapping("/transfer")
@GlobalTransactional
public boolean transfer(@RequestBody TransferVo transferVo) {
// 转出方
accountService.add(new AccountDO(transferVo.getFrom(), -1 * transferVo.getAccount()));
// 转入方
accountService.add(new AccountDO(transferVo.getTo(), transferVo.getAccount()));
// int i = 1 / 0; // 人为制造异常
return true;
}
}
@Data
/**
* 转账参数VO
*/
class TransferVo {
private String from;
private String to;
double account;
}
编写application.yml
配置文件:
server:
port: 8888
spring:
application:
name: seata-validate-demo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/seata_demo
cloud:
nacos: # 需要依赖注册中心进行远程调用
discovery:
enabled: true
namespace: public
server-addr: 127.0.0.1:8848
seata:
enabled: true
application-id: seata-server
# Seata 事务组编号,用于 TC 集群名
tx-service-group: default
service:
vgroupMapping:
default: default
seata-validate-demo-group: default
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # 对应nacos地址
group: SEATA_GROUP
data-id: seataServer.properties # 对应【分布式事务命名空间 ruoyi_seata】下创建ruoyiSeata.properties配置文件
namespace: # 对应分布式事务命名空间
registry:
type: nacos
nacos:
application: seata-server # 对应`conf/application.yml`中registry.nacos.application
server-addr: 127.0.0.1:8848 # 对应nacos地址
cluster: default
group: SEATA_GROUP
namespace: # 对应分布式事务命名空间
项目整体结构参考如下:
完成编码和配置以后,启动项目,进行转账接口测试,请求之前:
进行请求:
此时查看应用后端,发现应用报错:
java.lang.ArrayIndexOutOfBoundsException: 0
at io.seata.core.rpc.processor.client.ClientOnResponseProcessor.process(ClientOnResponseProcessor.java:103) ~[seata-all-2.0.0.jar:2.0.0]
at io.seata.core.rpc.netty.AbstractNettyRemoting.processMessage(AbstractNettyRemoting.java:306) ~[seata-all-2.0.0.jar:2.0.0]
at io.seata.core.rpc.netty.AbstractNettyRemotingClient$ClientHandler.channelRead(AbstractNettyRemotingClient.java:411) [seata-all-2.0.0.jar:2.0.0]
通过查看seata-server
端,也发现报错日志:
21:55:25.432 ERROR --- [rverHandlerThread_1_6_500] [pc.netty.AbstractNettyRemoting] [bda$processMessage$2] [192.168.239.1:8091:6161483039933980673] : 0104
==>
java.lang.NoClassDefFoundError: Could not initialize class io.seata.server.cluster.raft.RaftServerFactory$SingletonHandler
at io.seata.server.cluster.raft.RaftServerFactory.getInstance(RaftServerFactory.java:78) ~[classes!/:2.0.0]
at io.seata.server.session.GlobalSession.removeBranch(GlobalSession.java:352) ~[classes!/:2.0.0]
at io.seata.server.session.SessionHelper.removeBranch(SessionHelper.java:429) ~[classes!/:2.0.0]
at io.seata.server.coordinator.DefaultCore.lambda$doGlobalRollback$3(DefaultCore.java:325) ~[classes!/:2.0.0]
如果出现上面错误,需要在nacos
配置中心上seata
配置增加如下配置:
server.enableParallelRequestHandle=false
增加配置以后,重启seata
和应用,重新请求:
请求完成以后:
可以看到此时转账成功了。
我们人为制造一下异常,查看一下异常情况下能不能正常回滚,修改代码:
@RequestMapping("/transfer")
@GlobalTransactional
public boolean transfer(@RequestBody TransferVo transferVo) {
// 转出方
accountService.add(new AccountDO(transferVo.getFrom(), -1 * transferVo.getAccount()));
// 转入方
accountService.add(new AccountDO(transferVo.getTo(), transferVo.getAccount()));
int i = 1 / 0; // 人为制造异常
return true;
}
重启应用,进行请求:
查看数据库,数据没有发生变化:
可以看到,此时seata
进行分布式事务管理是成功的,你也可以在方法执行完成之前进行断点,然后查看客户端undo_log
,此时可以看到SQL镜像数据哦。
本次主要是介绍seata 2.0
版本的AT
模式入门,后续有时间会给大家介绍TCC
模式、Saga
模式以及seata
的底层原理,和我们自定义的一些扩展,欢迎大家点点关注。
更多文章信息:Spring Cloud与Seata 2.0集成:零基础安装到AT模式实战的完整教程
部署资源下载和完整demo参考下载地址:https://download.youkuaiyun.com/download/C_AJing/89693120?spm=1001.2014.3001.5503