分布式事务(八):Seata之AT模式应用
Seata中AT模式的官方文档地址:https://seata.io/zh-cn/docs/dev/mode/at-mode.html。
AT模式是一种无侵入的分布式事务解决方案,基于两阶段提交协议实现的,用户的业务SQL处理作为一阶段,Seata会根据一阶段的执行结果自动判断二阶段的提交或回滚。
1、整体机制
Seata的AT模式是基于两阶段提交协议演变而来的:
一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
二阶段:
提交:提交异步化
回滚:通过一阶段的回滚日志数据 undo_log 进行反向补偿。
2、应用
拿官方文档的案例进行操作,首先创建商品表 stock:
CREATE TABLE `stock` ( `product_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品id', `name` varchar(100) DEFAULT NULL COMMENT '商品名称', `stock` int DEFAULT '0' COMMENT '库存数量', PRIMARY KEY (`product_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='产品表'; INSERT INTO `stock`(`product_id`, `name`, `stock`) VALUES (1, 'JAVA编程思想', 2000);
订单表order:
CREATE TABLE `order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id', `product_id` bigint DEFAULT NULL COMMENT '商品id', `product_num` int DEFAULT NULL COMMENT '商品数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
回滚表undo_log:
-- 注意此处0.7.0+ 增加字段 context 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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.1、创建订单、库存服务
订单、库存服务目录结构如下:
订单、库存服务的POM依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.3.12.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.3.12.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.7.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> </dependencies>
2.1.1、订单服务
1.1、配置文件信息
appication.yml,文件中关于seata的配置需要与Seata-Server中的application.yml保持一致,Seata-Server的搭建详见分布式事务(七):Seata-Server的搭建。
server: port: 6021 spring: application: name: seata-order # druid数据源 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.33.55:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: root type: com.alibaba.druid.pool.DruidDataSource cloud: nacos: discovery: server-addr: 192.168.33.55:8848 alibaba: seata: tx-service-group: SEATA-GROUP # 事务组, 自定义需要与Seata-Server中的配置保持一致 seata: tx-service-group: trxgroup service: vgroup-mapping: trxgroup: default config: # support: nacos, consul, apollo, zk, etcd3 type: nacos nacos: server-addr: 192.168.33.55:8848 namespace: group: SEATA_GROUP username: password: data-id: seataServer.properties registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: 192.168.33.55:8848 group: SEATA_GROUP namespace: cluster: default username: nacos password: nacos store: # support: file 、 db 、 redis mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true user: root password: root min-conn: 5 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 100 max-wait: 5000 #设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: # 建立连接后从服务器读取到可用资源所用的时间 6s ReadTimeout: 6000 # 建立连接所用的时间 6s ConnectTimeout: 6000
1.2、订单服务业务代码
Seata事务模式配置,DataSourceConfig.java详情如下:
1 package com.snails.conf; 2 3 import com.alibaba.druid.pool.DruidDataSource; 4 import io.seata.rm.datasource.DataSourceProxy; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 7 import org.springframework.context.annotation.Bean; 8 import org.springframework.context.annotation.Configuration; 9 import javax.sql.DataSource; 10 11 @Configuration 12 public class DataSourceConfig { 13 14 @Autowired 15 DataSourceProperties dataSourceProperties; 16 17 /** 18 * SEATA事务模式配置 19 * @param dataSourceProperties 20 * @return 21 */ 22 @Bean 23 public DataSource dataSource(DataSourceProperties dataSourceProperties){ 24 DruidDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build(); 25 // AT 模式 26 return new DataSourceProxy(dataSource); 27 // XA 模式 28 // return new DataSourceProxyXA(dataSource); 29 } 30 }
orderMapper详情如下:
1 package com.snails.mapper; 2 import org.apache.ibatis.annotations.Insert; 3 import org.apache.ibatis.annotations.Mapper; 4 import org.apache.ibatis.annotations.Param; 5 6 @Mapper 7 public interface OrderMapper { 8 9 @Insert("INSERT INTO `order`(`product_id`, `product_num`) VALUES (#{productId}, #{productNum})") 10 Integer addOrder(@Param("productId") Integer productId, 11 @Param("productNum") Integer productNum); 12 13 }
OrderService详情如下:
1 package com.snails.service; 2 3 public interface OrderService { 4 Integer handleOrder(Integer productId); 5 }
OpenFeignService详情如下:
1 package com.snails.openfeign; 2 3 import org.springframework.cloud.openfeign.FeignClient; 4 import org.springframework.stereotype.Service; 5 import org.springframework.web.bind.annotation.GetMapping; 6 import org.springframework.web.bind.annotation.RequestParam; 7 8 @Service 9 @FeignClient("seata-stock") 10 public interface OpenFeignService { 11 @GetMapping("/handleStock") 12 Integer handleStock(@RequestParam("productId") Integer productId); 13 }
OrderServiceImpl详情如下:
1 package com.snails.service.impl; 2 3 import com.snails.openfeign.OpenFeignService; 4 import com.snails.mapper.OrderMapper; 5 import com.snails.service.OrderService; 6 import io.seata.spring.annotation.GlobalTransactional; 7 import org.springframework.stereotype.Service; 8 import org.springframework.transaction.annotation.Transactional; 9 import javax.annotation.Resource; 10 11 @Service 12 public class OrderServiceImpl implements OrderService { 13 14 @Resource 15 private OpenFeignService openFeignService; 16 17 @Resource 18 private OrderMapper orderMapper; 19 20 @Override 21 // 本地事务 22 @Transactional 23 // seata 全局事务 24 @GlobalTransactional 25 public Integer handleOrder(Integer productId) { 26 openFeignService.handleStock(productId); 27 // int i = 1/0; 28 orderMapper.addOrder(productId, 1); 29 return 1; 30 } 31 }
OrderController详情如下:
package com.snails.controller; import com.snails.openfeign.OpenFeignService; import com.snails.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderController { @Autowired private OrderService orderService; @GetMapping("/handleOrder") public Integer handleOrder(Integer productId) { orderService.handleOrder(productId); return 1; } }
SeataOrder6021Application详情如下:
1 package com.snails; 2 3 import org.mybatis.spring.annotation.MapperScan; 4 import org.springframework.boot.SpringApplication; 5 import org.springframework.boot.autoconfigure.SpringBootApplication; 6 import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 7 import org.springframework.cloud.openfeign.EnableFeignClients; 8 9 @SpringBootApplication 10 @EnableDiscoveryClient 11 @EnableFeignClients 12 @MapperScan("com.snails.mapper") 13 public class SeataOrder6021Application { 14 public static void main(String[] args) { 15 SpringApplication.run(SeataOrder6021Application.class, args); 16 } 17 }
1.3、启动订单服务
出现如上信息,订单服务搭建完成。
2.1.2、库存服务
2.1、配置文件信息
appication.yml:
1 server: 2 port: 6061 3 spring: 4 application: 5 name: seata-stock 6 datasource: 7 driver-class-name: com.mysql.cj.jdbc.Driver 8 url: jdbc:mysql://192.168.33.55:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true 9 username: root 10 password: root 11 type: com.alibaba.druid.pool.DruidDataSource 12 cloud: 13 nacos: 14 discovery: 15 server-addr: 192.168.33.55:8848 16 alibaba: 17 seata: 18 tx-service-group: SEATA-GROUP 19 20 seata: 21 tx-service-group: trxgroup 22 service: 23 vgroup-mapping: 24 trxgroup: default 25 config: 26 # support: nacos, consul, apollo, zk, etcd3 27 type: nacos 28 nacos: 29 server-addr: 192.168.33.55:8848 30 namespace: 31 group: SEATA_GROUP 32 username: 33 password: 34 data-id: seataServer.properties 35 registry: 36 # support: nacos, eureka, redis, zk, consul, etcd3, sofa 37 type: nacos 38 nacos: 39 application: seata-server 40 server-addr: 192.168.33.55:8848 41 group: SEATA_GROUP 42 namespace: 43 cluster: default 44 username: nacos 45 password: nacos 46 store: 47 # support: file 、 db 、 redis 48 mode: db 49 db: 50 datasource: druid 51 db-type: mysql 52 driver-class-name: com.mysql.cj.jdbc.Driver 53 url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true 54 user: root 55 password: root 56 min-conn: 5 57 max-conn: 100 58 global-table: global_table 59 branch-table: branch_table 60 lock-table: lock_table 61 distributed-lock-table: distributed_lock 62 query-limit: 100 63 max-wait: 5000 64 #设置feign客户端超时时间(OpenFeign默认支持ribbon) 65 ribbon: 66 # 建立连接后从服务器读取到可用资源所用的时间 6s 67 ReadTimeout: 6000 68 # 建立连接所用的时间 6s 69 ConnectTimeout: 6000
2.2、库存服务业务代码
StockServiceMapper详情如下:
1 package com.snails.mapper; 2 3 import org.apache.ibatis.annotations.Mapper; 4 import org.apache.ibatis.annotations.Param; 5 import org.apache.ibatis.annotations.Update; 6 7 @Mapper 8 public interface StockServiceMapper { 9 10 @Update("update stock set stock = stock - 1 where product_id = #{productId}") 11 Integer handleStock(@Param("productId") Integer productId); 12 13 }
StockService详情如下:
1 package com.snails.service; 2 3 public interface StockService { 4 Integer handleStock(Integer productId); 5 }
StockServiceImpl详情如下:
1 package com.snails.service.impl; 2 3 import com.snails.service.StockService; 4 import com.snails.mapper.StockServiceMapper; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.stereotype.Service; 7 8 @Service 9 public class StockServiceImpl implements StockService { 10 11 @Autowired 12 private StockServiceMapper stockMapper; 13 14 @Override 15 public Integer handleStock(Integer productId) { 16 return stockMapper.handleStock(productId); 17 } 18 }
StockController详情如下:
1 package com.snails.controller; 2 3 import com.snails.service.StockService; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.web.bind.annotation.GetMapping; 6 import org.springframework.web.bind.annotation.RequestParam; 7 import org.springframework.web.bind.annotation.RestController; 8 import java.util.Objects; 9 10 @RestController 11 public class StockController { 12 13 @Autowired 14 private StockService stockService; 15 16 @GetMapping("/handleStock") 17 public Integer handleStock(@RequestParam("productId") Integer productId) { 18 if (Objects.isNull(productId) || productId < 0) { 19 return -1; 20 } 21 return stockService.handleStock(productId); 22 } 23 }
SeataStock6061Application详情如下:
1 package com.snails; 2 3 import org.mybatis.spring.annotation.MapperScan; 4 import org.springframework.boot.SpringApplication; 5 import org.springframework.boot.autoconfigure.SpringBootApplication; 6 import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 7 8 @SpringBootApplication 9 @EnableDiscoveryClient 10 @MapperScan("com.snails.mapper") 11 public class SeataStock6061Application { 12 public static void main(String[] args) { 13 SpringApplication.run(SeataStock6061Application.class, args); 14 } 15 }
2.3、启动库存服务
出现如上日志信息,库存服务搭建完成。
2.2、分布式事务验证
2.2.1、无异常信息的验证
在订单服务的OrderServiceImpl中,注释异常代码。
浏览器访问接口:
订单表数据:
库存表数据:
2.2.2、有异常信息的验证
在订单服务的OrderServiceImpl中,放开异常代码,同时以debug模式启动订单服务,便于观察undo_log表记录。
undo_log表记录详情如下,回滚信息中记录更新前后数据记录。
浏览器访问接口:
库存服务日志详情:
订单服务日志详情:
2.3、搭建过程中的问题
2.3.1、无法连接到seata-server服务端
使用sh seata-server.sh 启动seata-server服务端,客户端接入时报错:
问题原因:
解决方案:
将配置文件做如下调整
同时将seata-server服务端的application.yml文件中有关seata的注册中心、配置中心配置复制到客户端的application.yml配置文件中,配置详情如下:
seata: tx-service-group: trxgroup service: vgroup-mapping: trxgroup: default config: # support: nacos, consul, apollo, zk, etcd3 type: nacos nacos: server-addr: 192.168.33.55:8848 namespace: group: SEATA_GROUP username: password: data-id: seataServer.properties registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: 192.168.33.55:8848 group: SEATA_GROUP namespace: cluster: default username: nacos password: nacos store: # support: file 、 db 、 redis mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true user: root password: root min-conn: 5 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 100 max-wait: 5000
2.3.2、Invalid bound statement (not found)
页面访问请求:
后台报错:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
原因:
启动类未扫描到Mapper的程序。
代码问题:
1、OrderMapper随意写在了service包下
2、启动类中@MapperSacn未指定扫描的包
解决方案:
1、新建mapper文件夹,将OrderMapper.java移至此文件夹下
2、启动类的@MapperScan注解中添加需要扫描的包
2.3.3、openFeign访问请求方式问题
问题:
原因:
在OpenFeign远程调用请求,在Order的OpenFeignService的handleStock方法里,参数未用@RequestParam注解修饰,OpenFeign默认使用Post请求方式,而在库存服务中,StockController中的handleStock方法使用的是GetMapping,即get请求方式。
解决方案:
在方法参数中添加@RequestParam注解。
在使用Seata的AT模式,必须创建undo_log表。如果未创建,在本例中会在库存服务中报错,提示回滚表不存在,报错详情如下: