springBoot集成seata
前置条件:安装nacos和mysql数据库
一丶下载seata服务端安装包,将下载的安装包上传至Linux并解压
1.2-1.4版本都可以
官网地址:https://github.com/seata/seata/releases
[root@iZuf6f6me43woqf6q431tqZ mysoft]# ls docker-compose jdk1.8.0_212 maven-3.6.3 mysql-8.0.23 nginx-1.20.1 node redis-6.2.1 seata seata-server-1.2.0.tar.gz tomcat-9.0.45 [root@iZuf6f6me43woqf6q431tqZ mysoft]#
二、在nacos中新建一个名称空间专门用来管理seata服务
三丶进入解压后的seata/conf目录,编辑registry.conf文件内容如下,即将type设为nacos,并填写自己的nacos配置信息,表示将seata服务端注册到nacos中
备注:application,cluster,group的值建议和我填一样,后面集成seata的时候要用,不然客户端启动时可能会找不到服务或者其他坑
type = "nacos" nacos { application = "serverAddr" serverAddr = "xxx.xxx.xxx.xxx:8848" # 填写你自己的nacos服务地址 namespace = "113db2b2-3a00-4bab-af8d-5f823c8815e5" # 填写你刚才在nacos中新建的名称空间id cluster = "default" group = "DEFAULT_GROUP" }
四丶到seata/bin目录下启动seata
后台启动:nohup ./seata-server.sh -h 127.0.0.1 -p 8091 >log.out 2>1 & 指定ip
备注:(1)这里最好将127.0.0.1换成你的Linux外网地址,不然客户端启动时可能会找不到服务,(2)如果启动时报内存不足等错误,可以编辑seata-server.sh文件,将2048改成256
若启动没有异常,打开nacos,查看seta服务是否已经注册到nacos中
五、准备数据库
新建4张表,account(账户表)、order_info(订单表)、storage(库存表)、undo_log(事务回滚表)
/* Navicat Premium Data Transfer Source Server : aliyun Source Server Type : MySQL Source Server Version : 80023 Source Host : 47.100.205.138:3306 Source Schema : seata Target Server Type : MySQL Target Server Version : 80023 File Encoding : 65001 Date: 28/03/2022 14:04:13 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for account -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint(0) NULL DEFAULT NULL COMMENT '用户id', `total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度', `used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用余额', `residue` decimal(10, 0) NULL DEFAULT 0 COMMENT '剩余可用额度', `is_deleted` int(0) NULL DEFAULT NULL COMMENT '删除状态:0-未删除,1-已删除', `update_user` bigint(0) NULL DEFAULT NULL COMMENT '更新人', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `create_user` bigint(0) NULL DEFAULT NULL COMMENT '创建人', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for order_info -- ---------------------------- DROP TABLE IF EXISTS `order_info`; CREATE TABLE `order_info` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `user_id` bigint(0) NULL DEFAULT NULL COMMENT '用户id', `product_id` bigint(0) NULL DEFAULT NULL COMMENT '产品id', `count` int(0) NULL DEFAULT NULL COMMENT '数量', `money` decimal(11, 0) NULL DEFAULT NULL COMMENT '金额', `status` int(0) NULL DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结', `is_deleted` int(0) NULL DEFAULT NULL COMMENT '删除状态:0-未删除,1-已删除', `update_user` bigint(0) NULL DEFAULT NULL COMMENT '更新人', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `create_user` bigint(0) NULL DEFAULT NULL COMMENT '创建人', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for storage -- ---------------------------- DROP TABLE IF EXISTS `storage`; CREATE TABLE `storage` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `product_id` bigint(0) NULL DEFAULT NULL COMMENT '产品id', `total` int(0) NULL DEFAULT NULL COMMENT '总库存', `used` int(0) NULL DEFAULT NULL COMMENT '已用库存', `residue` int(0) NULL DEFAULT NULL COMMENT '剩余库存', `is_deleted` int(0) NULL DEFAULT NULL COMMENT '删除状态:0-未删除,1-已删除', `update_user` bigint(0) NULL DEFAULT NULL COMMENT '更新人', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `create_user` bigint(0) NULL DEFAULT NULL COMMENT '创建人', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `undo_log` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `branch_id` bigint(0) 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(0) NOT NULL, `log_created` datetime(0) NOT NULL, `log_modified` datetime(0) NOT NULL, `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
向account和storage表初始化一条数据(user_id和product_id写死为1,后面下单时要用)
INSERT INTO account ( user_id, total, used, residue ) VALUES( 1, 100, 0, 100 ); INSERT INTO STORAGE ( product_id, total, used, residue ) VALUES(1,100,0,100)
六、搭建一个简易的微服务工程。具体细节我就不展示了,我的项目大致结构如下。服务之间通过openfeign调用(gateway网关服务暂时是一个空模块,没有意义,请求直接访问具体的服务,不走网关)
顶级模块依赖如下
七、引入相关依赖,这里我在最顶级的pom定义依赖的版本号,然后在common模块根据需求,引入具体的依赖
(ps:我这里同时引入了spring-cloud-alibaba-seata 和 seata-spring-boot-starter两个依赖,一是主要是使用springBoot方式集成方式比较方便,直接编写相关yml文件即可,二是cloud-alibaba-seata可以自动在服务调用链中传递xid)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <packaging>pom</packaging> <modules> <module>wu-order</module> <module>wu-common</module> <module>wu-account</module> <module>wu-storage</module> <module>wu-gateway</module> </modules> <groupId>com.wu.demo</groupId> <artifactId>springCloudAlibaba</artifactId> <version>1.0-SNAPSHOT</version> <!--继承springboot父项目--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR6</spring-cloud.version> <spring.cloud.alibaba.version>2.2.1.RELEASE</spring.cloud.alibaba.version> <mybatis.plus.version>3.2.0</mybatis.plus.version> <knife4j.version>2.0.8</knife4j.version> <seata.cloud.version>2.2.0.RELEASE</seata.cloud.version> <seata.boot.version>1.3.0</seata.boot.version> </properties> <!--维护公共jar包版本--> <dependencyManagement> <dependencies> <!--引入springcloud alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--引入springcloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--seata分布式事务spring-cloud集成--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>${seata.cloud.version}</version> </dependency> <!--seata分布式事务spring-boot集成--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>${seata.boot.version}</version> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.plus.version}</version> </dependency> <!--mybatis-plus代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis.plus.version}</version> </dependency> <!--knife4j-swagger增强版--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>${knife4j.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
common模块依赖如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>wu-common</artifactId> <groupId>com.wu.demo</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>common-web</artifactId> <!--维护所有微服务可能用到的公共依赖--> <dependencies> <!--spring相关--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--nacos-服务发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--引入openfegin--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--seata 分布式事务--> <!-- <dependency>--> <!-- <groupId>io.seata</groupId>--> <!-- <artifactId>seata-spring-boot-starter</artifactId>--> <!-- </dependency>--> <!--seata 分布式事务(使用这种方式可以自动传递xid)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <exclusions> <exclusion> <artifactId>druid</artifactId> <groupId>com.alibaba</groupId> </exclusion> </exclusions> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--mybatis-plus代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--knife4j-swagger增强版--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> </dependencies> </project>
八、编写yml文件,将seata集成到项目中。这里我只写和seata相关的配置
在每个服务的yml文件中填入以下代码(appliaction.name根据所在服务自定义)
spring: application: name: order cloud: nacos: discovery: server-addr: xxx.xxx.xxx.xxx:8848 # nacos地址 # seata配置 seata: enabled: true enable-auto-data-source-proxy: false # 关闭自动代理数据源 application-id: ${spring.application.name} tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default registry: type: nacos nacos: application: serverAddr server-addr: xxx.xxx.xxx.xxx:8848 # nacos地址 namespace: 113db2b2-3a00-4bab-af8d-5f823c8815e5 # seata服务所在的名称空间 group: DEFAULT_GROUP cluster: default config: file: name: file.conf
九、为了避免我们引入的mybatis-plus失效,需要在每个服务下引入以下两个配置类
@Data @Configuration public class DataSourceConfig { @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Value("${spring.datasource.driver-class-name}") private String driveClassName; }
@Configuration public class DataSourceProxyConfig { @Autowired private DataSourceConfig dataSourceConfig; @Bean("dataSource") public DataSource druidDataSource() { HikariDataSource hikariDataSource = new HikariDataSource (); hikariDataSource.setUsername(dataSourceConfig.getUsername()); hikariDataSource.setPassword(dataSourceConfig.getPassword()); hikariDataSource.setJdbcUrl(dataSourceConfig.getUrl()); hikariDataSource.setDriverClassName(dataSourceConfig.getDriveClassName()); return hikariDataSource; } @Bean @Primary public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } }
十、在每个服务的启动类下排除掉默认的数据源
exclude = DataSourceAutoConfiguration.class
十一、seata集成到此基本完成,下面我们编写具体的业务代码来测试其是否能完成全局事务的回滚。
我这里的业务是,用户下单,订单服务创建订单,然后调用账户服务扣减账户余额,然后调用库存服务扣减库存。其中账户服务和库存服务在更新数据之前会校验下余额或库存是否充足,若不足,则抛出异常。若其中任何一个环节出现异常后,前面的事务能够回滚,说明我们的seata生效了。
大致业务代码如下(注意,在事务发起方要开启全局事务注解@GlobalTransactional(rollbackFor = Exception.class))
订单服务:
账户服务:
库存服务:
十二、发起下单请求(user_id和product_id填写我们前面在数据总中初始化的数据,这里count和money故意写大一些,使其校验不通过,抛出异常,然后观察order_info表是否有数据插入,account和storage表是否被更新)
十二、发起请求后,如预期的一样,抛出异常,并且order_info表是空的并没有插入数据,account和storage表也没有被更新,说明我们的全局事务生效了。观察order服务的日志输出,确实有回滚记录。(ps:通过在异常之前打断点可知,在抛出异常之前订单数据其实已经插入到了订单表中,并且undo_log表中也有数据,但是由于后序业务出现异常,seata根据undo_log中的数据将订单表中的数据回滚,最后清除undo_log中的数据)
结语:seata集成当你最后完成时看似简单,但整个过程确实很艰难,耗时费力,在集成过程中总是会出现各种错误,如seata server启动报错,客户端启动找不到服务,全局事务不回滚,mybatis-plus失效,代理数据源不生效等等,基本上网上哪些seata集成的各种错误我都遇到了,不得不说seata的整合的确很不友好,没有极大的耐心很难坚持下来。上面的步骤,也只是我对最终效果的总结,参考一下可以,但可能并不适合每一个人。大家遇到报错时还是具体问题具体分析。