Spring Cloud Alibaba Seata分布式事务组件使用教程
什么是分布式事务
数据库中的事务是数据库写数据时出现错误时保证数据一致性的机制,单体服务中依靠数据库中的事务机制就能保证数据一致性
在分布式服务中,一个业务功能,会涉及多个服务节点,每个服务节点都有需要开启一个事务来保证数据的一致性
在分布式服务的相互调用中,有时会出现某个服务节点出现故障,导致某个环节数据没有成功保存,出现数据不一致的情况
分布式事务就是为解决这种情况导致数据不一致的机制,在一个业务链路中,当某个服务出现故障,则这个链路涉及的数据库操作都要回滚保证数据的一致性
- 案例
- 用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持
- 仓储服务∶对给定的商品扣除仓库/商品数量
- 订单服务;根据采购需求创建订单
- 帐户服务∶从用户帐户中扣除余额
- 单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源
- 业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证
- 但是全局的数据—致性问题没法保证
- 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
什么是Seata
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
Seata提供的分布式事务方案
Seata AT 模式
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等
整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
Seata TCC 模式
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。
整体机制
- 在两阶段提交协议中,资源管理器(RM, Resource Manager)需要提供“准备”、“提交”和“回滚” 3 个操作;而事务管理器(TM, Transaction Manager)分 2 阶段协调所有资源管理器,在第一阶段询问所有资源管理器“准备”是否成功,如果所有资源均“准备”成功则在第二阶段执行所有资源的“提交”操作,否则在第二阶段执行所有资源的“回滚”操作,保证所有资源的最终状态是一致的,要么全部提交要么全部回滚。
- 资源管理器有很多实现方式,其中 TCC(Try-Confirm-Cancel)是资源管理器的一种服务化的实现;TCC 是一种比较成熟的分布式事务解决方案,可用于解决跨数据库、跨服务业务操作的数据一致性问题;TCC 其 Try、Confirm、Cancel 3 个方法均由业务编码实现,故 TCC 可以被称为是服务化的资源管理器。
- TCC 的 Try 操作作为一阶段,负责资源的检查和预留;Confirm 操作作为二阶段提交操作,执行真正的业务;Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。
Seata Saga 模式
Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Seata XA 模式
XA 模式是从 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
整体机制
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
基于Seata AT 模式分布式事务案例
- 术语解读
- Transaction ID(XID):全局唯一的事务ID
- Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
- Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
- Resource Manager(RM):控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
- 流程解读
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
-
下载安装Seata
-
配置Seata
- 参考"seata-server/conf/application.example.yml"配置文件的配置,进行"seata-server/conf/application.yml"配置文件的配置
- 配置配置中心和注册中心使用nacos
- 数据存储模式使用MySQL数据库
# # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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: 8091 spring: application: name: seata-server main: web-application-type: none logging: config: classpath:logback-spring.xml file: path: ${log.home:${user.home}/logs/seata} extend: logstash-appender: # off by default enabled: false destination: 127.0.0.1:4560 kafka-appender: # off by default enabled: false bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash producer: acks: 0 linger-ms: 1000 max-block-ms: 0 metric-appender: # off by default enabled: false seata: config: # support: nacos, consul, apollo, zk, etcd3 type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUP data-id: seataServer.properties registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: public cluster: default store: # support: file 、 db 、 redis 、 raft mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true user: root password: hsp min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock vgroup-table: vgroup_table query-limit: 1000 max-wait: 5000 druid: time-between-eviction-runs-millis: 120000 min-evictable-idle-time-millis: 300000 test-while-idle: true test-on-borrow: false keep-alive: false # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000'
-
在MySQL数据库中创建一个seata数据库
create database seata; use seata;
-
运行"seata-server/script/server/db/mysql.sql"SQL文件,创建Seata需要的数据表
-- -------------------------------- 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); CREATE TABLE IF NOT EXISTS `vgroup_table` ( `vGroup` VARCHAR(255), `namespace` VARCHAR(255), `cluster` VARCHAR(255), UNIQUE KEY `idx_vgroup_namespace_cluster` (`vGroup`,`namespace`,`cluster`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-
运行Seata
-
先后运行nacos和seata,验证seata能成功启动
-
运行nacos
startup.cmd -m standalone
-
运行seata,使用"seata-server/bin/seata-server.bat"脚本进行启动
seata-server.bat
-
-
查看nacos控制台,验证seata注册到nacos
-
创建三个对应的微服务模块
-
创建三个微服务需要使用的数据库和数据表
-
订单微服务的数据库
-- 订单微服务的数据库 CREATE DATABASE order_micro_service ; USE order_micro_service; CREATE TABLE `order`( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT DEFAULT NULL, product_id BIGINT DEFAULT NULL, nums INT DEFAULT NULL, money INT DEFAULT NULL, `status` INT DEFAULT NULL COMMENT '0:创建中;1:已完结' );
- 创建Seata需要使用的"undo_log"数据表
-- 注意此处0.3.0+ 增加唯一索引 ux_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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
库存微服务的数据库
-- 库存微服务的数据库 CREATE DATABASE storage_micro_service; USE storage_micro_service; CREATE TABLE `storage`( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, product_id BIGINT DEFAULT NULL, amount INT DEFAULT NULL COMMENT'库存量' ); -- 初始化库存表 INSERT INTO `storage` VALUES(NULL,1,10);
- 创建Seata需要使用的"undo_log"数据表
-- 注意此处0.3.0+ 增加唯一索引 ux_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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
账号微服务的数据库
-- 账号微服务的数据库 CREATE DATABASE account_micro_service; USE account_micro_service; CREATE TABLE `account`( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT DEFAULT NULL, money INT DEFAULT NULL COMMENT'账户金额' ); -- 初始化账户表 INSERT INTO `account`VALUES(NULL,666,10000);
- 创建Seata需要使用的"undo_log"数据表
-- 注意此处0.3.0+ 增加唯一索引 ux_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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
-
搭建订单微服务模块
-
新建一个"seata_order_micro_service"模块
-
配置依赖
<!--导入seata依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </exclusion> <exclusion> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </exclusion> <exclusion> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </exclusion> </exclusions> </dependency> <!--nacos服务发现的依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--配合OpenFeign的负载均衡组件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!--远程调用openFeign的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>3.1.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>anyi.space</groupId> <artifactId>common</artifactId> <version>${project.version}</version> </dependency>
-
配置"application.yaml"
server: port: 10012 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/order_micro_service?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver application: name: seata-order-micro-service cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: tx-service-group: seata-server mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: io: seata: info
-
编写业务代码
-
domain
package space.anyi.seataOrderMicroService.domain; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import lombok.Data; /** * * @TableName order */ @TableName(value ="order") @Data public class Order implements Serializable { /** * */ @TableId(type = IdType.AUTO) private Long id; /** * */ private Long userId; /** * */ private Long productId; /** * */ private Integer nums; /** * */ private Integer money; /** * 0:创建中;1:已完结 */ private Integer status; @TableField(exist = false) private static final long serialVersionUID = 1L; @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null) { return false; } if (getClass() != that.getClass()) { return false; } Order other = (Order) that; return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId())) && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId())) && (this.getProductId() == null ? other.getProductId() == null : this.getProductId().equals(other.getProductId())) && (this.getNums() == null ? other.getNums() == null : this.getNums().equals(other.getNums())) && (this.getMoney() == null ? other.getMoney() == null : this.getMoney().equals(other.getMoney())) && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus())); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getId() == null) ? 0 : getId().hashCode()); result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode()); result = prime * result + ((getProductId() == null) ? 0 : getProductId().hashCode()); result = prime * result + ((getNums() == null) ? 0 : getNums().hashCode()); result = prime * result + ((getMoney() == null) ? 0 : getMoney().hashCode()); result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode()); return result; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ["); sb.append("Hash = ").append(hashCode()); sb.append(", id=").append(id); sb.append(", userId=").append(userId); sb.append(", productId=").append(productId); sb.append(", nums=").append(nums); sb.append(", money=").append(money); sb.append(", status=").append(status); sb.append(", serialVersionUID=").append(serialVersionUID); sb.append("]"); return sb.toString(); } }
-
mapper
package space.anyi.seataOrderMicroService.mapper; import org.apache.ibatis.annotations.Mapper; import space.anyi.seataOrderMicroService.domain.Order; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 杨逸 * @description 针对表【order】的数据库操作Mapper * @createDate 2025-09-21 11:22:55 * @Entity generator.domain.Order */ @Mapper public interface OrderMapper extends BaseMapper<Order> { }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="space.anyi.seataOrderMicroService.mapper.OrderMapper"> <resultMap id="BaseResultMap" type="space.anyi.seataOrderMicroService.domain.Order"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="userId" column="user_id" jdbcType="BIGINT"/> <result property="productId" column="product_id" jdbcType="BIGINT"/> <result property="nums" column="nums" jdbcType="INTEGER"/> <result property="money" column="money" jdbcType="INTEGER"/> <result property="status" column="status" jdbcType="INTEGER"/> </resultMap> <sql id="Base_Column_List"> id,user_id,product_id, nums,money,status </sql> </mapper>
-
service
package space.anyi.seataOrderMicroService.service; import space.anyi.seataOrderMicroService.domain.Order; import com.baomidou.mybatisplus.extension.service.IService; /** * @author 杨逸 * @description 针对表【order】的数据库操作Service * @createDate 2025-09-21 11:22:55 */ public interface OrderService extends IService<Order> { void saveOrder(Order order); }
package space.anyi.seataOrderMicroService.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import space.anyi.seataOrderMicroService.domain.Order; import space.anyi.seataOrderMicroService.service.AccountService; import space.anyi.seataOrderMicroService.service.OrderService; import space.anyi.seataOrderMicroService.mapper.OrderMapper; import org.springframework.stereotype.Service; import space.anyi.seataOrderMicroService.service.StorageService; /** * @author 杨逸 * @description 针对表【order】的数据库操作Service实现 * @createDate 2025-09-21 11:22:55 */ @Slf4j @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private AccountService accountService; @Autowired private StorageService storageService; @Override public void saveOrder(Order order) { log.info("开始创建订单"); save(order); log.info("减少金额开始"); accountService.reduce(order.getUserId(),order.getMoney()); log.info("减少金额结束"); log.info("减少库存开始"); storageService.reduce(order.getProductId(),order.getNums()); log.info("减少库存结束"); order.setStatus(0); updateById(order); log.info("订单创建成功"); } }
package space.anyi.seataOrderMicroService.service; import anyi.sapce.common.entity.ResponseResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @ProjectName: distributedSystemLearn * @FileName: AccountService * @Author: 杨逸 * @Data:2025/9/21 11:35 * @Description: */ @FeignClient("seata-account-micro-service") public interface AccountService { @PostMapping("/account/reduce") public ResponseResult reduce(@RequestParam("userId")Long userId, @RequestParam("money")Integer money); }
package space.anyi.seataOrderMicroService.service; import anyi.sapce.common.entity.ResponseResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @ProjectName: distributedSystemLearn * @FileName: StorageService * @Author: 杨逸 * @Data:2025/9/21 11:36 * @Description: */ @FeignClient("seata-storage-micro-service") public interface StorageService { @PostMapping("/storage/reduce") public ResponseResult reduce(@RequestParam("productId") Long productId,@RequestParam("nums") Integer nums); }
-
controller
package space.anyi.seataOrderMicroService.controller; import anyi.sapce.common.entity.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import space.anyi.seataOrderMicroService.domain.Order; import space.anyi.seataOrderMicroService.service.OrderService; /** * @ProjectName: distributedSystemLearn * @FileName: OrderController * @Author: 杨逸 * @Data:2025/9/21 11:25 * @Description: */ @RestController public class OrderController { @Autowired private OrderService orderService; @GetMapping("/order/save") public ResponseResult save(Order order){ orderService.saveOrder(order); return ResponseResult.okResult("订单创建成功",null); } }
-
主启动类
package space.anyi.seataOrderMicroService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @ProjectName: distributedSystemLearn * @FileName: SeataOrderMicroServiceApplication * @Author: 杨逸 * @Data:2025/9/21 11:24 * @Description: */ @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class SeataOrderMicroServiceApplication { public static void main(String[] args) { SpringApplication.run(SeataOrderMicroServiceApplication.class,args); } }
-
-
验证搭建成功
-
-
搭建库存微服务模块
-
创建"seata_storage_micro_service"模块
-
导入依赖
<!--导入seata依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!--nacos服务发现的依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--远程调用openFeign的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>3.1.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>anyi.space</groupId> <artifactId>common</artifactId> <version>${project.version}</version> </dependency>
-
配置application.yaml
server: port: 10010 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/storage_micro_service?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver application: name: seata-storage-micro-service cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: tx-service-group: seata-server mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: io: seata: info
-
编写业务代码
-
domain
package space.anyi.seataStorageMicroService.domain; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import lombok.Data; /** * * @TableName storage */ @TableName(value ="storage") @Data public class Storage implements Serializable { /** * */ @TableId(type = IdType.AUTO) private Long id; /** * */ private Long productId; /** * 库存量 */ private Integer amount; @TableField(exist = false) private static final long serialVersionUID = 1L; @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null) { return false; } if (getClass() != that.getClass()) { return false; } Storage other = (Storage) that; return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId())) && (this.getProductId() == null ? other.getProductId() == null : this.getProductId().equals(other.getProductId())) && (this.getAmount() == null ? other.getAmount() == null : this.getAmount().equals(other.getAmount())); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getId() == null) ? 0 : getId().hashCode()); result = prime * result + ((getProductId() == null) ? 0 : getProductId().hashCode()); result = prime * result + ((getAmount() == null) ? 0 : getAmount().hashCode()); return result; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ["); sb.append("Hash = ").append(hashCode()); sb.append(", id=").append(id); sb.append(", productId=").append(productId); sb.append(", amount=").append(amount); sb.append(", serialVersionUID=").append(serialVersionUID); sb.append("]"); return sb.toString(); } }
-
mapper
package space.anyi.seataStorageMicroService.mapper; import org.apache.ibatis.annotations.Mapper; import space.anyi.seataStorageMicroService.domain.Storage; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 杨逸 * @description 针对表【storage】的数据库操作Mapper * @createDate 2025-09-20 16:56:26 * @Entity generator.domain.Storage */ @Mapper public interface StorageMapper extends BaseMapper<Storage> { void reduce(Long productId, Integer nums); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="space.anyi.seataStorageMicroService.mapper.StorageMapper"> <resultMap id="BaseResultMap" type="space.anyi.seataStorageMicroService.domain.Storage"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="productId" column="product_id" jdbcType="BIGINT"/> <result property="amount" column="amount" jdbcType="INTEGER"/> </resultMap> <sql id="Base_Column_List"> id,product_id,amount </sql> <update id="reduce"> UPDATE storage SET amount = amount - #{nums} WHERE product_id = #{productId} </update> </mapper>
-
service
package space.anyi.seataStorageMicroService.service; import space.anyi.seataStorageMicroService.domain.Storage; import com.baomidou.mybatisplus.extension.service.IService; /** * @author 杨逸 * @description 针对表【storage】的数据库操作Service * @createDate 2025-09-20 16:56:26 */ public interface StorageService extends IService<Storage> { void reduce(Long productId, Integer nums); }
package space.anyi.seataStorageMicroService.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import space.anyi.seataStorageMicroService.domain.Storage; import space.anyi.seataStorageMicroService.service.StorageService; import space.anyi.seataStorageMicroService.mapper.StorageMapper; import org.springframework.stereotype.Service; /** * @author 杨逸 * @description 针对表【storage】的数据库操作Service实现 * @createDate 2025-09-20 16:56:26 */ @Slf4j @Service public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService{ @Override public void reduce(Long productId, Integer nums) { log.info("扣减库存开始"); getBaseMapper().reduce(productId,nums); log.info("扣减库存结束"); } }
-
controller
package space.anyi.seataStorageMicroService.controller; import anyi.sapce.common.entity.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import space.anyi.seataStorageMicroService.service.StorageService; /** * @ProjectName: distributedSystemLearn * @FileName: StorageController * @Author: 杨逸 * @Data:2025/9/20 16:59 * @Description: */ @RestController public class StorageController { @Autowired private StorageService storageService; //扣减库存 @PostMapping("/storage/reduce") public ResponseResult reduce(Long productId, Integer nums){ storageService.reduce(productId,nums); return ResponseResult.okResult("扣减库存成功ok",null); } }
-
主启动类
package space.anyi.seataStorageMicroService; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @ProjectName: distributedSystemLearn * @FileName: SeataStorageMicroServiceApplication * @Author: 杨逸 * @Data:2025/9/20 16:53 * @Description: */ @MapperScan("space.anyi.seataStorageMicroService.mapper") @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication() public class SeataStorageMicroServiceApplication { public static void main(String[] args) { SpringApplication.run(SeataStorageMicroServiceApplication.class,args); } }
-
-
验证搭建成功
-
-
搭建账号微服务模块
-
创建"account_micro_service"模块
-
导入依赖
<!--导入seata依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </exclusion> <exclusion> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </exclusion> <exclusion> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </exclusion> </exclusions> </dependency> <!--nacos服务发现的依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--远程调用openFeign的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>3.1.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>anyi.space</groupId> <artifactId>common</artifactId> <version>${project.version}</version> </dependency>
-
配置aoolication.yaml
server: port: 10011 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/account_micro_service?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver application: name: seata-account-micro-service cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: tx-service-group: seata-server mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: io: seata: info
-
编写业务代码
-
domain
package space.anyi.seataAccountMicroService.domain; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import lombok.Data; /** * * @TableName account */ @TableName(value ="account") @Data public class Account implements Serializable { /** * */ @TableId(type = IdType.AUTO) private Long id; /** * */ private Long userId; /** * 账户金额 */ private Integer money; @TableField(exist = false) private static final long serialVersionUID = 1L; @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null) { return false; } if (getClass() != that.getClass()) { return false; } Account other = (Account) that; return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId())) && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId())) && (this.getMoney() == null ? other.getMoney() == null : this.getMoney().equals(other.getMoney())); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getId() == null) ? 0 : getId().hashCode()); result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode()); result = prime * result + ((getMoney() == null) ? 0 : getMoney().hashCode()); return result; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ["); sb.append("Hash = ").append(hashCode()); sb.append(", id=").append(id); sb.append(", userId=").append(userId); sb.append(", money=").append(money); sb.append(", serialVersionUID=").append(serialVersionUID); sb.append("]"); return sb.toString(); } }
-
mapper
package space.anyi.seataAccountMicroService.mapper; import space.anyi.seataAccountMicroService.domain.Account; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * @author 杨逸 * @description 针对表【account】的数据库操作Mapper * @createDate 2025-09-21 10:52:56 * @Entity generator.domain.Account */ public interface AccountMapper extends BaseMapper<Account> { void reduce(Long userId, Integer money); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="space.anyi.seataAccountMicroService.mapper.AccountMapper"> <resultMap id="BaseResultMap" type="space.anyi.seataAccountMicroService.domain.Account"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="userId" column="user_id" jdbcType="BIGINT"/> <result property="money" column="money" jdbcType="INTEGER"/> </resultMap> <sql id="Base_Column_List"> id,user_id,money </sql> <update id="reduce"> UPDATE account SET money = money - #{money} WHERE user_id = #{userId}; </update> </mapper>
-
service
package space.anyi.seataAccountMicroService.service; import space.anyi.seataAccountMicroService.domain.Account; import com.baomidou.mybatisplus.extension.service.IService; /** * @author 杨逸 * @description 针对表【account】的数据库操作Service * @createDate 2025-09-21 10:52:56 */ public interface AccountService extends IService<Account> { void reduce(Long userId, Integer money); }
package space.anyi.seataAccountMicroService.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import space.anyi.seataAccountMicroService.domain.Account; import space.anyi.seataAccountMicroService.service.AccountService; import space.anyi.seataAccountMicroService.mapper.AccountMapper; import org.springframework.stereotype.Service; /** * @author 杨逸 * @description 针对表【account】的数据库操作Service实现 * @createDate 2025-09-21 10:52:56 */ @Service @Slf4j public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService{ @Override public void reduce(Long userId, Integer money) { log.info("扣款开始"); getBaseMapper().reduce(userId, money); log.info("扣款结束"); } }
-
controller
package space.anyi.seataAccountMicroService.controller; import anyi.sapce.common.entity.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import space.anyi.seataAccountMicroService.service.AccountService; /** * @ProjectName: distributedSystemLearn * @FileName: AccountController * @Author: 杨逸 * @Data:2025/9/21 10:59 * @Description: */ @RestController public class AccountController { @Autowired private AccountService accountService; @PostMapping("/account/reduce") public ResponseResult reduce(@RequestParam("userId")Long userId, @RequestParam("money")Integer money){ accountService.reduce(userId,money); return ResponseResult.okResult(200,"扣减账户余额OK"); } }
-
主启动类
package space.anyi.seataAccountMicroService; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @ProjectName: distributedSystemLearn * @FileName: SeataAccountMicroServiceApplication * @Author: 杨逸 * @Data:2025/9/21 10:50 * @Description: */ @MapperScan("space.anyi.seataAccountMicroService.mapper") @SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class SeataAccountMicroServiceApplication { public static void main(String[] args) { SpringApplication.run(SeataAccountMicroServiceApplication.class,args); } }
-
-
验证搭建成功
-
-
-
接口测试,验证三个微服务模块之间的调用链路通畅
- 接口调用
-
数据一致性验证,查看数据表的数据修改是一致的
-
模拟一个异常导致链路调用失败,使得数据不一致
-
在"seata-storage-micro-service"服务的业务中加入一段睡眠代码,模拟调用超时
package space.anyi.seataStorageMicroService.controller; import anyi.sapce.common.entity.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import space.anyi.seataStorageMicroService.service.StorageService; import java.util.concurrent.TimeUnit; /** * @ProjectName: distributedSystemLearn * @FileName: StorageController * @Author: 杨逸 * @Data:2025/9/20 16:59 * @Description: */ @RestController public class StorageController { @Autowired private StorageService storageService; //扣减库存 @PostMapping("/storage/reduce") public ResponseResult reduce(Long productId, Integer nums){ //模拟一个延迟,使得调用链路超时,导致的调用失败 try { TimeUnit.SECONDS.sleep(12); } catch (InterruptedException e) { e.printStackTrace(); } storageService.reduce(productId,nums); return ResponseResult.okResult("扣减库存成功ok",null); } }
-
在调用方配置openfeign的超时时间
feign: client: config: #配置超时时间 seata-storage-micro-service: #读取资源超时时间 readTimeout: 6000 #建立连接超时时间 connectTimeout: 1000
-
验证出现异常导致的数据不一致问题
-
-
使用seata解决分布式事务
-
在"seata-order-micro-service"服务中配置seata
seata: registry: # 配置seata客户端的注册中心, 告诉seata client 怎么去访问seata server(TC) type: nacos nacos: # seata server 所在的nacos服务地址 server-addr: 127.0.0.1:8848 # seata server 的服务名seata-server ,如果没有修改可以不配 application: seata-server # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配 group: SEATA_GROUP cluster: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP #这里每个服务都是对应不同的映射名,在配置中心可以看到 tx-service-group: order_tx_group service: vgroup-mapping: order_tx_group: default
在nacos创建对应的配置
-
在"seata-storage-micro-service"服务中配置seata
seata: registry: # 配置seata客户端的注册中心, 告诉seata client 怎么去访问seata server(TC) type: nacos nacos: # seata server 所在的nacos服务地址 server-addr: 127.0.0.1:8848 # seata server 的服务名seata-server ,如果没有修改可以不配 application: seata-server # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配 group: SEATA_GROUP cluster: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP #这里每个服务都是对应不同的映射名,在配置中心可以看到 tx-service-group: storage_tx_group service: vgroup-mapping: storage_tx_group: default
在nacos创建对应的配置
-
在"seata-account-micro-service"服务中配置seata
seata: registry: # 配置seata客户端的注册中心, 告诉seata client 怎么去访问seata server(TC) type: nacos nacos: # seata server 所在的nacos服务地址 server-addr: 127.0.0.1:8848 # seata server 的服务名seata-server ,如果没有修改可以不配 application: seata-server # seata server 所在的组,默认就是SEATA_GROUP,没有改也可以不配 group: SEATA_GROUP cluster: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP #这里每个服务都是对应不同的映射名,在配置中心可以看到 tx-service-group: account_tx_group service: vgroup-mapping: account_tx_group: default
在nacos上创建对应的配置
-
在业务链路调用入口使用"GlobalTransactional"注解开启分布式事务
package space.anyi.seataOrderMicroService.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import space.anyi.seataOrderMicroService.domain.Order; import space.anyi.seataOrderMicroService.service.AccountService; import space.anyi.seataOrderMicroService.service.OrderService; import space.anyi.seataOrderMicroService.mapper.OrderMapper; import org.springframework.stereotype.Service; import space.anyi.seataOrderMicroService.service.StorageService; /** * @author 杨逸 * @description 针对表【order】的数据库操作Service实现 * @createDate 2025-09-21 11:22:55 */ @Slf4j @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private AccountService accountService; @Autowired private StorageService storageService; //使用@GlobalTransactional开启分布式事务 //name为事务名称,rollbackFor指定回滚的异常类型 @GlobalTransactional(name = "seata_order_micro_service",rollbackFor = Exception.class) @Override public void saveOrder(Order order) { log.info("开始创建订单"); save(order); log.info("减少金额开始"); accountService.reduce(order.getUserId(),order.getMoney()); log.info("减少金额结束"); log.info("减少库存开始"); storageService.reduce(order.getProductId(),order.getNums()); log.info("减少库存结束"); order.setStatus(0); updateById(order); log.info("订单创建成功"); } }
-
未验证成功
出现错误
### Error updating database. Cause: java.sql.SQLException: io.seata.common.loader.EnhancedServiceNotFoundException: not found service provider for : io.seata.rm.datasource.sql.struct.TableMetaCache