什么是分布式事务问题?
单体应用
单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务来保证。
微服务应用
随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
小结
在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,一次业务操作需要操作多个数据源或需要进行远程调用,就会产生分布式事务问题。
Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 组成
Transaction ID(XID)
全局唯一的事务id
三组件
Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接受事务协调的指令,驱动分支(本地)事务的提交和回滚
Seata 分布式事务处理过程
过程图:
说明:
1、TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
2、XID 在微服务调用链路的上下文中传播;
3、RM 向 TC 注册分支事务,将其纳入 XID 对应的全局事务的管辖;
4、TM 向 TC 发起针对 XID 的全局提交或回滚决议;
5、TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata 部署
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。
项目结构图:
Seata服务端(TC)部署
1、下载服务端压缩包,地址:https://github.com/seata/seata/releases
本例下载的是 seata-server-1.2.0.tar.gz,并解压
2、修改事务日志存储模式为 db
及数据库连接信息,即修改 conf目录中 flie.conf 文件,如下:
1 ## transaction log store, only used in seata-server 2 store { 3 ## store mode: file、db 4 mode = "db" 5 6 ## file store property 7 file { 8 ## store location dir 9 dir = "sessionStore" 10 # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions 11 maxBranchSessionSize = 16384 12 # globe session size , if exceeded throws exceptions 13 maxGlobalSessionSize = 512 14 # file buffer size , if exceeded allocate new buffer 15 fileWriteBufferCacheSize = 16384 16 # when recover batch read size 17 sessionReloadReadSize = 100 18 # async, sync 19 flushDiskMode = async 20 } 21 22 ## database store property 23 db { 24 ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. 25 datasource = "druid" 26 ## mysql/oracle/postgresql/h2/oceanbase etc. 27 dbType = "mysql" 28 driverClassName = "com.mysql.cj.jdbc.Driver" 29 url = "jdbc:mysql://localhost:3306/seata" 30 user = "admin" 31 password = "123456" 32 minConn = 5 33 maxConn = 30 34 globalTable = "global_table" 35 branchTable = "branch_table" 36 lockTable = "lock_table" 37 queryLimit = 100 38 maxWait = 5000 39 } 40 }
由于我们使用了db模式存储事务日志,所以我们需要创建一个seat数据库,建表sql在seat项目的github找到,
地址:https://github.com/seata/seata/tree/1.2.0/script/server/db 目录 mysql.sql 中
1 -- -------------------------------- The script used when storeMode is 'db' -------------------------------- 2 -- the table to store GlobalSession data 3 CREATE TABLE IF NOT EXISTS `global_table` 4 ( 5 `xid` VARCHAR(128) NOT NULL, 6 `transaction_id` BIGINT, 7 `status` TINYINT NOT NULL, 8 `application_id` VARCHAR(32), 9 `transaction_service_group` VARCHAR(32), 10 `transaction_name` VARCHAR(128), 11 `timeout` INT, 12 `begin_time` BIGINT, 13 `application_data` VARCHAR(2000), 14 `gmt_create` DATETIME, 15 `gmt_modified` DATETIME, 16 PRIMARY KEY (`xid`), 17 KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), 18 KEY `idx_transaction_id` (`transaction_id`) 19 ) ENGINE = InnoDB 20 DEFAULT CHARSET = utf8; 21 22 -- the table to store BranchSession data 23 CREATE TABLE IF NOT EXISTS `branch_table` 24 ( 25 `branch_id` BIGINT NOT NULL, 26 `xid` VARCHAR(128) NOT NULL, 27 `transaction_id` BIGINT, 28 `resource_group_id` VARCHAR(32), 29 `resource_id` VARCHAR(256), 30 `branch_type` VARCHAR(8), 31 `status` TINYINT, 32 `client_id` VARCHAR(64), 33 `application_data` VARCHAR(2000), 34 `gmt_create` DATETIME(6), 35 `gmt_modified` DATETIME(6), 36 PRIMARY KEY (`branch_id`), 37 KEY `idx_xid` (`xid`) 38 ) ENGINE = InnoDB 39 DEFAULT CHARSET = utf8; 40 41 -- the table to store lock data 42 CREATE TABLE IF NOT EXISTS `lock_table` 43 ( 44 `row_key` VARCHAR(128) NOT NULL, 45 `xid` VARCHAR(96), 46 `transaction_id` BIGINT, 47 `branch_id` BIGINT NOT NULL, 48 `resource_id` VARCHAR(256), 49 `table_name` VARCHAR(32), 50 `pk` VARCHAR(36), 51 `gmt_create` DATETIME, 52 `gmt_modified` DATETIME, 53 PRIMARY KEY (`row_key`), 54 KEY `idx_branch_id` (`branch_id`) 55 ) ENGINE = InnoDB 56 DEFAULT CHARSET = utf8;
3、修改注册中心,使用nacos作为注册中心,即修改 conf目录中 registry.conf 文件,如下:
1 registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 type = "nacos" 4 5 nacos { 6 application = "seata-server" 7 serverAddr = "localhost:8848" 8 namespace = "" 9 cluster = "default" 10 username = "" 11 password = "" 12 } 13 }
4、启动Nacos服务(参考:【SpringCloud】Spring Cloud Alibaba 之 Nacos注册中心(二十七))、在启动Seata服务
Seata服务启动命令:sh ./bin/seata-server.sh
Seata客户端(TM和RM)部署
业务数据库准备
订单库order
1 CREATE DATABASE seata_order; 2 3 USE seata_order; 4 5 CREATE TABLE `order` ( 6 `id` bigint(11) NOT NULL AUTO_INCREMENT, 7 `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', 8 `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', 9 `count` int(11) DEFAULT NULL COMMENT '数量', 10 `money` decimal(11,0) DEFAULT NULL COMMENT '金额', 11 `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结', 12 PRIMARY KEY (`id`) 13 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
库存库storage
1 USE seata_storage; 2 3 CREATE TABLE `storage` ( 4 `id` bigint(11) NOT NULL AUTO_INCREMENT, 5 `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', 6 `total` int(11) DEFAULT NULL COMMENT '总库存', 7 `used` int(11) DEFAULT NULL COMMENT '已用库存', 8 `residue` int(11) DEFAULT NULL COMMENT '剩余库存', 9 PRIMARY KEY (`id`) 10 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 11 12 INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
账户库
1 CREATE DATABASE seata_account; 2 3 USE seata_account; 4 5 6 CREATE TABLE `account` ( 7 `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id', 8 `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', 9 `total` decimal(10,0) DEFAULT NULL COMMENT '总额度', 10 `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额', 11 `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度', 12 PRIMARY KEY (`id`) 13 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 14 15 INSERT INTO `seata_account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
在三个库中都插入undo_log表,sql地址:https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql
1 -- for AT mode you must to init this sql for you business database. the seata server not need it. 2 CREATE TABLE IF NOT EXISTS `undo_log` 3 ( 4 `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', 5 `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', 6 `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', 7 `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', 8 `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', 9 `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', 10 `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', 11 UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) 12 ) ENGINE = InnoDB 13 AUTO_INCREMENT = 1 14 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
三个数据库
Order订单服务
1、新建订单模块(springcloud-seata-order9011)
2、编辑pom文件,完整pom如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>test-springcloud</artifactId> 7 <groupId>com.test</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>springcloud-seata-order9011</artifactId> 13 14 <dependencies> 15 16 <!-- seata--> 17 <dependency> 18 <groupId>com.alibaba.cloud</groupId> 19 <artifactId>spring-cloud-alibaba-seata</artifactId> 20 <exclusions> 21 <exclusion> 22 <groupId>io.seata</groupId> 23 <artifactId>seata-all</artifactId> 24 </exclusion> 25 </exclusions> 26 </dependency> 27 <dependency> 28 <groupId>io.seata</groupId> 29 <artifactId>seata-all</artifactId> 30 <version>1.2.0</version> 31 </dependency> 32 33 <!-- alibaba nacos discovery --> 34 <dependency> 35 <groupId>com.alibaba.cloud</groupId> 36 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> 37 </dependency> 38 39 <dependency> 40 <groupId>org.springframework.cloud</groupId> 41 <artifactId>spring-cloud-starter-openfeign</artifactId> 42 </dependency> 43 44 <!-- spring boot --> 45 <dependency> 46 <groupId>org.springframework.boot</groupId> 47 <artifactId>spring-boot-starter-web</artifactId> 48 </dependency> 49 <dependency> 50 <groupId>org.springframework.boot</groupId> 51 <artifactId>spring-boot-starter-actuator</artifactId> 52 </dependency> 53 <dependency> 54 <groupId>org.springframework.boot</groupId> 55 <artifactId>spring-boot-devtools</artifactId> 56 <scope>runtime</scope> 57 <optional>true</optional> 58 </dependency> 59 60 <dependency> 61 <groupId>org.springframework.boot</groupId> 62 <artifactId>spring-boot-starter-jdbc</artifactId> 63 </dependency> 64 65 <dependency> 66 <groupId>org.mybatis.spring.boot</groupId> 67 <artifactId>mybatis-spring-boot-starter</artifactId> 68 </dependency> 69 70 <!-- mysql --> 71 <dependency> 72 <groupId>mysql</groupId> 73 <artifactId>mysql-connector-java</artifactId> 74 </dependency> 75 76 <dependency> 77 <groupId>org.projectlombok</groupId> 78 <artifactId>lombok</artifactId> 79 <optional>true</optional> 80 </dependency> 81 <dependency> 82 <groupId>org.springframework.boot</groupId> 83 <artifactId>spring-boot-starter-test</artifactId> 84 <scope>test</scope> 85 </dependency> 86 87 </dependencies> 88 89 </project>
注意seata-all版本是:1.2.0,与服务端版本一致
3、编辑application.yml属性文件
1 # 端口 2 server: 3 port: 9011 4 5 spring: 6 application: 7 name: seata-order-service 8 # 数据源基本配置 9 cloud: 10 nacos: 11 discovery: 12 server-addr: localhost:8848 13 alibaba: 14 seata: 15 # 此处的名称一定要与 vgroup-mapping配置的参数保持一致 16 tx-service-group: my_order_tx_group 17 datasource: 18 driver-class-name: com.mysql.cj.jdbc.Driver 19 url: jdbc:mysql://localhost:3306/seata_order?allowPublicKeyRetrieval=true&useSSL=true 20 username: admin 21 password: 123456 22 hikari: 23 connection-test-query: SELECT 1 FROM DUAL 24 minimum-idle: 1 25 maximum-pool-size: 10 26 pool-name: ${spring.application.name}-CP 27 idle-timeout: 10000 28 cachePrepStmts: true 29 prepStmtCacheSize: 250 30 prepStmtCacheSqlLimit: 2048 31 leakDetectionThreshold: 40000 32 33 ribbon: 34 ReadTimeout: 600000 35 ConnectTimeout: 600000 36 MaxAutoRetries: 0 37 MaxAutoRetriesNextServer: 1 38 39 # 饿加载开启 Feign 预加载, 防止第一次请求超时 40 eager-load: 41 enabled: true 42 clients: seata-storage-service, storage-account-server 43 44 mybatis: 45 mapperLocations: classpath:mapper/*Mapper.xml 46 # 所有entity别名类所在的包 47 type-aliases-pachage: com.test.springcloud.entities 48 49 logging: 50 level: 51 # root: debug 52 com.test.springcloud: debug
4、在resource目录中添加registry.conf配置文件,可服务服务端中的registry.conf文件如下:
1 registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 type = "nacos" 4 5 nacos { 6 application = "seata-server" 7 serverAddr = "localhost:8848" 8 namespace = "" 9 cluster = "default" 10 username = "" 11 password = "" 12 } 13 eureka { 14 serviceUrl = "http://localhost:8761/eureka" 15 application = "default" 16 weight = "1" 17 } 18 redis { 19 serverAddr = "localhost:6379" 20 db = 0 21 password = "" 22 cluster = "default" 23 timeout = 0 24 } 25 zk { 26 cluster = "default" 27 serverAddr = "127.0.0.1:2181" 28 sessionTimeout = 6000 29 connectTimeout = 2000 30 username = "" 31 password = "" 32 } 33 consul { 34 cluster = "default" 35 serverAddr = "127.0.0.1:8500" 36 } 37 etcd3 { 38 cluster = "default" 39 serverAddr = "http://localhost:2379" 40 } 41 sofa { 42 serverAddr = "127.0.0.1:9603" 43 application = "default" 44 region = "DEFAULT_ZONE" 45 datacenter = "DefaultDataCenter" 46 cluster = "default" 47 group = "SEATA_GROUP" 48 addressWaitTime = "3000" 49 } 50 file { 51 name = "file.conf" 52 } 53 } 54 55 config { 56 # file、nacos 、apollo、zk、consul、etcd3 57 type = "file" 58 59 nacos { 60 serverAddr = "localhost:8848" 61 namespace = "" 62 group = "SEATA_GROUP" 63 username = "" 64 password = "" 65 } 66 consul { 67 serverAddr = "127.0.0.1:8500" 68 } 69 apollo { 70 appId = "seata-server" 71 apolloMeta = "http://192.168.1.204:8801" 72 namespace = "application" 73 } 74 zk { 75 serverAddr = "127.0.0.1:2181" 76 sessionTimeout = 6000 77 connectTimeout = 2000 78 username = "" 79 password = "" 80 } 81 etcd3 { 82 serverAddr = "http://localhost:2379" 83 } 84 file { 85 name = "file.conf" 86 } 87 }
5、在resource目录中添加file.conf配置文件,内容如下;
1 transport { 2 # tcp udt unix-domain-socket 3 type = "TCP" 4 #NIO NATIVE 5 server = "NIO" 6 #enable heartbeat 7 heartbeat = true 8 # the client batch send request enable 9 enableClientBatchSendRequest = true 10 #thread factory for netty 11 threadFactory { 12 bossThreadPrefix = "NettyBoss" 13 workerThreadPrefix = "NettyServerNIOWorker" 14 serverExecutorThread-prefix = "NettyServerBizHandler" 15 shareBossWorker = false 16 clientSelectorThreadPrefix = "NettyClientSelector" 17 clientSelectorThreadSize = 1 18 clientWorkerThreadPrefix = "NettyClientWorkerThread" 19 # netty boss thread size,will not be used for UDT 20 bossThreadSize = 1 21 #auto default pin or 8 22 workerThreadSize = "default" 23 } 24 shutdown { 25 # when destroy server, wait seconds 26 wait = 3 27 } 28 serialization = "seata" 29 compressor = "none" 30 } 31 32 service { 33 #transaction service group mapping 34 vgroupMapping.my_order_tx_group = "default" 35 #only support when registry.type=file, please don't set multiple addresses 36 default.grouplist = "127.0.0.1:8091" 37 #degrade, current not support 38 enableDegrade = false 39 #disable seata 40 disableGlobalTransaction = false 41 } 42 43 client { 44 rm { 45 asyncCommitBufferLimit = 10000 46 lock { 47 retryInterval = 10 48 retryTimes = 30 49 retryPolicyBranchRollbackOnConflict = true 50 } 51 reportRetryCount = 5 52 tableMetaCheckEnable = false 53 reportSuccessEnable = false 54 } 55 tm { 56 commitRetryCount = 5 57 rollbackRetryCount = 5 58 } 59 undo { 60 dataValidation = true 61 logSerialization = "jackson" 62 logTable = "undo_log" 63 } 64 log { 65 exceptionRate = 100 66 } 67 }
注意其中的:vgroupMapping.my_order_tx_group = "default"配置
6、编辑主启动类
1 @EnableFeignClients 2 // 自动代理数据源 3 @EnableAutoDataSourceProxy 4 @EnableDiscoveryClient 5 @SpringBootApplication 6 public class SeataOrder9011 { 7 public static void main(String[] args) { 8 SpringApplication.run(SeataOrder9011.class, args); 9 } 10 }
7、编辑业务实现类
7.1、controller如下:
1 @Slf4j 2 @RestController 3 public class OrderController { 4 @Autowired 5 private OrderService orderService; 6 7 @GetMapping("/order/create") 8 public CommonResult create(Order order) { 9 orderService.create(order); 10 return new CommonResult(1, "success"); 11 } 12 }
7.2、service接口:
1 public interface OrderService { 2 3 // 新建订单 4 public void create(Order order); 5 6 }
7.3、service实现类
主要业务是:创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
@GlobalTransactional注解用以开启全局事务
1 @Service 2 @Slf4j 3 public class OrderServiceImpl implements OrderService { 4 5 @Autowired 6 private OrderDao orderDao; 7 8 @Autowired 9 private StorageService storageService; 10 11 @Autowired 12 private AccountService accountService; 13 14 /** 15 * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 16 */ 17 @GlobalTransactional(name = "my_test_tx_group",rollbackFor = Exception.class) 18 public void create(Order order) { 19 log.info("------->下单开始"); 20 //本应用创建订单 21 orderDao.insert(order); 22 23 //远程调用库存服务扣减库存 24 log.info("------->order-service中扣减库存开始"); 25 storageService.decrease(order.getProductId(),order.getCount()); 26 log.info("------->order-service中扣减库存结束:{}",order.getId()); 27 28 //远程调用账户服务扣减余额 29 log.info("------->order-service中扣减余额开始"); 30 accountService.decrease(order.getUserId(),order.getMoney()); 31 log.info("------->order-service中扣减余额结束"); 32 33 //修改订单状态为已完成 34 log.info("------->order-service中修改订单状态开始"); 35 orderDao.update(order.getId(), order.getUserId(),0); 36 log.info("------->order-service中修改订单状态结束"); 37 38 log.info("------->下单结束"); 39 } 40 41 }
7.5、StorageService的FeignClient
1 @FeignClient(value = "seata-storage-service") 2 public interface StorageService { 3 4 @PostMapping(value = "/storage/decrease") 5 CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count); 6 }
7.6、AccountService的FeignClient
1 @FeignClient(value = "seata-account-service") 2 public interface AccountService { 3 4 @PostMapping(value = "/account/decrease") 5 void decrease(@RequestParam("userId") Long userId,@RequestParam("money") BigDecimal money); 6 7 }
7.7、OrderDao
1 @Mapper 2 public interface OrderDao { 3 // 新建订单 4 public int insert(Order order); 5 6 // 更新订单 从0修改为1 7 public Order update(@Param("id") Long id, @Param("userId") Long userId, @Param("status") Integer status); 8 }
7.8、实体类CommonResult
1 @Data 2 @AllArgsConstructor 3 @NoArgsConstructor 4 public class CommonResult<T> { 5 6 private int code; 7 private String msg; 8 private T data; 9 10 public CommonResult(int code, String msg) { 11 this.code = code; 12 this.msg = msg; 13 } 14 }
7.9、实体类Order
1 @Data 2 @AllArgsConstructor 3 @NoArgsConstructor 4 public class Order { 5 private Long id; 6 private Long userId; 7 private Long productId; 8 private Integer count; 9 private BigDecimal money; 10 private Integer status; 11 12 }
7.10、在resource/mapper添加映射文件OrderMapper.xml
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3 4 <mapper namespace="com.test.springcloud.dao.OrderDao"> 5 6 <resultMap id="BaseResultMap" type="com.test.springcloud.entities.Order" > 7 <id property="id" jdbcType="BIGINT" column="id" /> 8 <result property="userId" jdbcType="BIGINT" column="user_id" /> 9 <result property="productId" jdbcType="BIGINT" column="product_id" /> 10 <result property="count" jdbcType="INTEGER" column="count" /> 11 <result property="money" jdbcType="DECIMAL" column="money" /> 12 <result property="status" jdbcType="INTEGER" column="status" /> 13 </resultMap> 14 15 <insert id="insert" parameterType="com.test.springcloud.entities.Order" useGeneratedKeys="true" 16 keyProperty="id"> 17 INSERT INTO `order` (id, user_id, product_id, count, money, status ) 18 values(null, #{userId}, #{productId}, #{count}, #{money}, 0) 19 </insert> 20 21 <select id="update" > 22 UPDATE `order` set status = 1 23 WHERE id = #{id} and status = #{status} and user_id = #{userId} 24 </select> 25 </mapper>
Storage库存服务
1、新建订单模块(springcloud-seata-storage9012)
2、编辑pom文件,同上
3、编辑application.yml属性文件,同上
1 # 端口 2 server: 3 port: 9012 4 5 spring: 6 application: 7 name: seata-storage-service 8 # 数据源基本配置 9 cloud: 10 nacos: 11 discovery: 12 server-addr: localhost:8848 13 alibaba: 14 seata: 15 tx-service-group: my_storage_tx_group 16 datasource: 17 driver-class-name: com.mysql.cj.jdbc.Driver 18 url: jdbc:mysql://localhost 3306/seata_storage?allowPublicKeyRetrieval=true&useSSL=true 19 username: admin 20 password: 123456 21 hikari: 22 connection-test-query: SELECT 1 FROM DUAL 23 minimum-idle: 1 24 maximum-pool-size: 10 25 pool-name: ${spring.application.name}-CP 26 idle-timeout: 10000 27 cachePrepStmts: true 28 prepStmtCacheSize: 250 29 prepStmtCacheSqlLimit: 2048 30 leakDetectionThreshold: 40000 31 32 feign.hystrix.enabled: true 33 hystrix: 34 command: 35 default: 36 circuitBreaker: 37 sleepWindowInMilliseconds: 30000 38 requestVolumeThreshold: 10 39 execution: 40 isolation: 41 strategy: SEMAPHORE 42 thread: 43 timeoutInMilliseconds: 100000 44 45 46 47 mybatis: 48 mapperLocations: classpath:mapper/*Mapper.xml 49 # 所有entity别名类所在的包 50 type-aliases-pachage: com.test.springcloud.entities 51 52 logging: 53 level: 54 # root: debug 55 com.test.springcloud: debug
4、在resource目录中添加registry.conf配置文件,同上
5、在resource目录中添加file.conf配置文件,同上
1 transport { 2 # tcp udt unix-domain-socket 3 type = "TCP" 4 #NIO NATIVE 5 server = "NIO" 6 #enable heartbeat 7 heartbeat = true 8 # the client batch send request enable 9 enableClientBatchSendRequest = true 10 #thread factory for netty 11 threadFactory { 12 bossThreadPrefix = "NettyBoss" 13 workerThreadPrefix = "NettyServerNIOWorker" 14 serverExecutorThread-prefix = "NettyServerBizHandler" 15 shareBossWorker = false 16 clientSelectorThreadPrefix = "NettyClientSelector" 17 clientSelectorThreadSize = 1 18 clientWorkerThreadPrefix = "NettyClientWorkerThread" 19 # netty boss thread size,will not be used for UDT 20 bossThreadSize = 1 21 #auto default pin or 8 22 workerThreadSize = "default" 23 } 24 shutdown { 25 # when destroy server, wait seconds 26 wait = 3 27 } 28 serialization = "seata" 29 compressor = "none" 30 } 31 32 service { 33 #transaction service group mapping 34 vgroupMapping.my_storage_tx_group = "default" 35 #only support when registry.type=file, please don't set multiple addresses 36 default.grouplist = "127.0.0.1:8091" 37 #degrade, current not support 38 enableDegrade = false 39 #disable seata 40 disableGlobalTransaction = false 41 } 42 43 client { 44 rm { 45 asyncCommitBufferLimit = 10000 46 lock { 47 retryInterval = 10 48 retryTimes = 30 49 retryPolicyBranchRollbackOnConflict = true 50 } 51 reportRetryCount = 5 52 tableMetaCheckEnable = false 53 reportSuccessEnable = false 54 } 55 tm { 56 commitRetryCount = 5 57 rollbackRetryCount = 5 58 } 59 undo { 60 dataValidation = true 61 logSerialization = "jackson" 62 logTable = "undo_log" 63 } 64 log { 65 exceptionRate = 100 66 } 67 }
注意其中的:vgroupMapping.my_storage_tx_group = "default"
7、编辑业务实现类
与上雷同,mapper.xml文件如下:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3 4 <mapper namespace="com.test.springcloud.dao.StorageDao"> 5 6 <resultMap id="BaseResultMap" type="com.test.springcloud.entities.Storage" > 7 <id property="id" jdbcType="BIGINT" column="id" /> 8 <result property="productId" jdbcType="BIGINT" column="product_id" /> 9 <result property="total" jdbcType="INTEGER" column="total" /> 10 <result property="used" jdbcType="INTEGER" column="used" /> 11 <result property="residue" jdbcType="INTEGER" column="residue" /> 12 </resultMap> 13 14 <select id="decrease" > 15 UPDATE storage 16 set used = used + #{count}, 17 residue = residue - #{count} 18 WHERE product_id = #{productId} 19 </select> 20 </mapper>
Account账户服务
1、新建订单模块(springcloud-seata-storage9012)
2、编辑pom文件,同上
3、编辑application.yml属性文件,同上
4、在resource目录中添加registry.conf配置文件,同上
5、在resource目录中添加file.conf配置文件,同上
注意其中的:vgroupMapping.my_account_tx_group = "default"
7、编辑业务实现类
与上雷同,mapper.xml文件如下:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3 4 <mapper namespace="com.test.springcloud.dao.AccountDao"> 5 6 <resultMap id="BaseResultMap" type="com.test.springcloud.entities.Account" > 7 <id property="id" jdbcType="BIGINT" column="id" /> 8 <result property="userId" jdbcType="BIGINT" column="user_id" /> 9 <result property="total" jdbcType="DECIMAL" column="total" /> 10 <result property="used" jdbcType="DECIMAL" column="used" /> 11 <result property="residue" jdbcType="DECIMAL" column="residue" /> 12 </resultMap> 13 14 <select id="decrease" > 15 UPDATE account 16 set used = used + #{money}, 17 residue = residue - #{money} 18 WHERE user_id = #{userId} 19 </select> 20 </mapper>
验证分布式事务
1、启动Nacos,然后启动Seata服务端
2、分别启动order,storage,account服务
3、查看Seata服务端控制台输出内容:
三个服务分别注册了 RM 和 TM,都用通道连接
4、浏览器访问地址:http://localhost:9011/order/create?userId=1&productId=1&count=10&money=100,创建订单,开始业务
5、查看数据库,order,storage,account,三个数据库数据的变化
order表
storage表
account表
三张表数据正常,符合正确的业务逻辑
6、在Account服务中,增加一个异常,模式业务失败
1 @Service 2 @Slf4j 3 public class AccountServiceImpl implements AccountService { 4 @Autowired 5 private AccountDao storageDao; 6 7 public void decrease(Long userId, BigDecimal money) { 8 log.info("------->account-service中扣减账户余额开始"); 9 // 模拟业务异常,全局事务回滚 10 int n = 10/0; 11 storageDao.decrease(userId, money); 12 log.info("------->account-service中扣减账户余额结束"); 13 } 14 }
7、重新启动Account服务,且访问地址:http://localhost:9011/order/create?userId=1&productId=1&count=10&money=100,创建订单,开始业务
8、请求报错,数据无变化,符合正常逻辑,验证Seata分布式事务管理已生效
9、还可以去掉Order服务中的@GlobalTransactional注解,然后重新启动Order服务
10、访问地址:http://localhost:9011/order/create?userId=1&productId=1&count=10&money=100,创建订单,开始业务
当服务报错时,查看数据库,三个数据库的数据异常,不符合正常逻辑