JB4-8-事务机制
Java道经第4卷 - 第8阶 - 事务机制
文章目录
- S01. Seata概念入门
- E01. 基础入门概念
- 1. 本地事务
- 2. 分布式事务
- 3. Seata基础概念
- E02. 搭建单机容器
- S02. Seata项目整合
- E01. 开发通用服务
- E02. 开发商品服务
- 1. 开发数据层 - 扣库存
- 2. 开发业务层 - 扣库存
- 3. 开发控制层 - 扣库存
- E03. 开发订单服务
- 1. 开发远程接口
- 2. 开发数据层 - 下订单
- 3. 开发业务层 - 下订单
- 4. 开发控制层 - 下订单
- E04. 添加事务保护
- 1. 添加主配项
- 2. 升级业务层 - 扣库存
- 3. 升级业务层 - 下订单
- 4. 测试事务保护效果
心法:本章使用 Maven 父子结构项目进行练习
练习项目结构如下:
|_ v4-8-micro-transaction|_ seata-common|_ 14801 seata-product|_ 14802 seata-order
武技:搭建练习项目结构
- 创建父项目 v4-8-micro-transaction,删除 src 目录。
- 在父项目中管理依赖:
<properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-boot.version>3.2.5</spring-boot.version><spring-cloud.version>2023.0.1</spring-cloud.version><spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version><junit.version>4.13.2</junit.version><lombok.version>1.18.24</lombok.version><hutool-all.version>5.8.25</hutool-all.version><mysql-connector-j.version>8.2.0</mysql-connector-j.version><mybatis-spring-boot-starter.version>3.0.4</mybatis-spring-boot-starter.version>
</properties><dependencyManagement><dependencies><!--spring-boot-starter-parent--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><!--spring-cloud-dependencies--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><!--spring-cloud-alibaba-dependencies--><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></dependencies>
</dependencyManagement>
- 在父项目中添加依赖:
<dependencies><!--junit--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope></dependency><!--hutool-all--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool-all.version}</version></dependency>
</dependencies>
S01. Seata概念入门
E01. 基础入门概念
1. 本地事务
心法:本地事务是数据库层面提供的核心事务机制,它如同一个精密的 “操作容器”,将一次业务处理中涉及的所有数据库操作(如增、删、改等)封装成一个不可分割的执行单元,在这个单元内,所有操作遵循 “要么全成,要么全败” 的铁律,只要其中任一操作执行失败(如数据校验出错、网络中断等),整个事务就会触发回滚机制,将数据库状态恢复到事务执行前的初始状态,以此确保业务数据的完整性与准确性。
本地事务 - 原子性(Atomicity):事务是一个不可拆分的最小执行单元,如同 “原子” 般不可再分:
- 一个事务中的所有操作要么全部成功提交,数据永久生效,要么在任一操作失败时,所有已执行的操作全部回滚,数据库回归到事务开始前的状态,不存在部分成功的中间态。
本地事务 - 一致性(Consistency):事务执行的前后,数据库必须始终处于逻辑一致的状态:
- 例如,在转账业务中,无论事务成功与否,转出账户与转入账户的总金额必须保持不变,若转出账户扣减金额后,转入账户未成功增加对应金额,事务会回滚以维持总金额的一致性。
本地事务 - 隔离性(Isolation):在多用户并发操作数据库时,不同事务之间如同被 “隔离墙” 分隔,彼此的操作互不干扰:
- 数据库通过定义不同的隔离级别(如读未提交、读已提交、可重复读、串行化),控制事务对共享数据的访问权限,避免脏读、不可重复读、幻读等并发问题,确保每个事务都能感知到一致的数据状态。
本地事务 - 持久性(Durability):一旦事务成功提交(即达到 “提交点”),它对数据库的所有修改都会被永久保存,即便后续发生数据库崩溃、服务器断电等意外情况,重启后数据仍能恢复到事务提交后的状态:
- 这一特性通过数据库的日志机制(如 redo 日志)实现,确保修改不会因系统故障丢失。
2. 分布式事务
心法:在微服务架构中,一个完整的业务流程往往需要多个微服务协同完成,而这些微服务可能部署在不同的服务器上,依赖不同的数据库,而分布式事务正是为了应对这种跨服务、跨数据库的场景,它要求将分布在多个微服务中的 N 个操作纳入同一个事务管理范畴,确保这些操作要么全部成功执行并提交,要么在任一环节失败时全部回滚,最终实现与本地事务一致的 ACID 特性,从而保证整个分布式系统的数据一致性。
与本地事务相比,分布式事务面临着更复杂的挑战:
- 跨节点通信:事务涉及的操作分布在不同微服务节点,网络延迟、中断等问题可能导致事务状态同步失败。
- 多数据源协调:每个微服务可能使用独立的数据库,需要协调多个数据源的事务提交或回滚,避免出现部分数据源提交、部分回滚的不一致状态。
- 性能与一致性平衡:分布式环境中,强一致性可能导致系统性能下降,因此需要在一致性与可用性之间寻找平衡点(如采用最终一致性方案)。
3. Seata基础概念
心法:Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴架构中的分布式事务解决方案,对业务无侵入,使得分布式事务的使用像本地事务的使用一样简单和高效,在 1.5.2 版本后,开始支持 MySQL8 版本。
Seata 组件:包含事务管理器,事务协调器和资源管理器三个核心组件:
- 事务管理器(Transaction Manager,简称 TM):决定何时何地全局提交或回滚事务,核心是 @GlobalTransactinal 注解。
- 事务协调器(Transaction Coordinator,简称 TC):负责与所有 RM 通信,协调全局事务,核心是 Seata Server 服务。
- 资源管理器(Resource Manager,简称 RM):负责发起分支事务,执行具体操作,核心是 Seata 依赖。
Seata 模式:提供了 AT(默认)、TCC、SAGA 和 XA 四种事务模式:
- TCC 模式:要求自己在各个分支业务逻辑层编写代码进行事务的提交和回滚:
- 优点:虽然代码是自己写的,但是事务整体提交或回滚的机制仍然可用。
- 缺点:每个业务都要编写三个方法来对应,代码冗余,而且业务入侵量大。
- SAGA 模式:核心思想是编写一个类,当指定的事务发生问题时,运行 SAGA 编写的回滚类:
- 优点:这样编写代码不影响已经编写好的业务逻辑代码,一般用于修改已经编写完成的老代码。
- 缺点:类数量多,开发量大。
- XA 模式:支持 XA 协议的数据库分布式事务,使用比较少,此处略讲。
- AT 模式:仅支持所有事务分支都是操作关系型数据库的场景,该模式采取两段式提交:
- 表决阶段:所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者。
- 执行阶段:协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚。
AT 模式流程详解:
武技:搭建 Seata 使用环境
- 创建 seata 专用数据库,并引入相关的 Seata 服务四张表:
-- 创建数据库
create database seata character set utf8mb4;
use seata;-- 引入四张表
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; 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; 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);
- 创建业务数据库,包括订单表,商品表和 UndoLog 日志表:
-- 创建数据库
create database micro_transaction character set utf8mb4;
use micro_transaction;-- 创建订单表
CREATE TABLE IF NOT EXISTS `order`
(`id` BIGINT AUTO_INCREMENT COMMENT '主键',`sn` VARCHAR(128) NOT NULL COMMENT '订单号',`product_id` BIGINT NOT NULL COMMENT '购买的商品ID',`number` INT NOT NULL COMMENT '购买的商品个数',primary key (`id`)
);-- 创建商品表
CREATE TABLE IF NOT EXISTS `product`
(`id` BIGINT NOT NULL COMMENT '主键',`title` VARCHAR(128) NOT NULL COMMENT '商品名称',`stock` BIGINT NOT NULL COMMENT '商品库存',primary key (`id`)
);-- 创建 UndoLog 表:UndoLog 属于业务表,不要在 seata 专用的数据库中添加
CREATE TABLE IF NOT EXISTS `undo_log`
(`branch_id` BIGINT(20) NOT NULL COMMENT '分支事务ID',`xid` VARCHAR(100) NOT NULL COMMENT '全局事务ID',`context` VARCHAR(128) NOT NULL COMMENT '上下文',`rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息',`log_status` INT(11) NOT NULL COMMENT '状态,0正常,1全局已完成',`log_created` DATETIME(6) NOT NULL COMMENT '创建时间',`log_modified` DATETIME(6) NOT NULL COMMENT '修改时间',UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT事务模式撤消表';-- 向商品表添加测试数据
insert into `product` (`id`, `title`, `stock`) values (1, '小米手机', 2);
insert into `product` (`id`, `title`, `stock`) values (2, '苹果手机', 4);
insert into `product` (`id`, `title`, `stock`) values (3, '华为手机', 200);
- 在 Nacos 管控台创建一个 Seata 专属的命名空间:
配置项 | 值 |
---|---|
命名空间ID | seata-namespace |
命名空间名 | seata |
- 点击标题栏的 seata/public 可切换命名空间。
- 在 seata 命名空间中添加 default_tx_group 配置,具体如下:
配置项 | 值 | 其它 |
---|---|---|
DataID | service.vgroupMapping.default_tx_group | 固定名称 |
Group | SEATA_GROUP | 自定义分组 |
配置格式 | TEXT | 配置文件格式 |
配置内容 | 内容如下 |
default
- 在 seata 命名空间中添加 seataServer.properties 配置,具体如下:
配置项 | 值 | 其它 |
---|---|---|
DataID | seataServer.properties | 固定名称 |
Group | SEATA_GROUP | 自定义分组 |
配置格式 | Properties | 配置文件格式 |
配置内容 | 内容如下 | 参考 seataServer.properties GitHub地址 |
注意 seataServer.properties 全部配置均存在,对位修改即可,不要添加,以免发生覆盖:
# 修改:事务信息存储位置,默认file
store.mode=db
# 修改:事务锁信息存储方式,默认file
store.lock.mode=db
# 修改:事务会话信息存储方式,默认file
store.session.mode=db
# 修改:db模式数据库url,修改为seata专用数据库
store.db.url=jdbc:mysql://192.168.40.77:3306/seata?useUnicode=true&rewriteBatchedStatements=true
# 修改:db模式数据库账户,修改为自己的账号
store.db.user=root
# 修改:db模式数据库密码,修改为自己的密码
store.db.password=root
# 修改:db模式数据库初始连接数,默认1,修改为5
store.db.minConn=5
# 修改:db模式数据库最大连接数,默认20,修改为30
store.db.maxConn=30
E02. 搭建单机容器
武技:在 Docker 中搭建 Seata 的事务协调器(TC)服务容器。
- 创建相关目录:
# 创建Seata相关目录
mkdir -p /opt/seata/conf;
chmod -R 777 /opt/seata;
- 创建临时容器,拷贝配置文件:
# 拉取镜像(二选一)
docker pull seataio/seata-server;
docker pull registry.cn-hangzhou.aliyuncs.com/joezhou/seata:1.7.0;# 创建临时容器: 仅为拷贝主配文件
docker run -d --name tmp registry.cn-hangzhou.aliyuncs.com/joezhou/seata:1.7.0;# 拷贝配置文件
docker cp tmp:/seata-server/resources/application.yml /opt/seata/conf/application.yml;# 删除临时文件
docker rm -f tmp
- 修改主配文件:
vim /opt/seata/conf/application.yml
修改内容如下:只要修改 config,registry 和 store 三块内容,其余配置默认即可:
seata:config:type: nacos # 使用Nacos作为配置中心nacos:server-addr: http://192.168.40.77:8848 # Nacos注册中心地址namespace: seata-namespace # 命名空间,使用默认的命名空间时可以不用填group: SEATA_GROUP # 分组名称data-id: seataServer.properties # seataServer配置文件registry:type: nacos # 使用Nacos作为注册中心nacos:application: seata-server # Seata在Nacos中的服务名称server-addr: http://192.168.40.77:8848 # Nacos配置中心地址namespace: seata-namespace # 命名空间,使用默认的命名空间时可以不用填group: SEATA_GROUP # 分组名称cluster: default # 集群名称,默认defaultstore:mode: db # 使用数据库存储模式db:datasource: druid # 使用druid连接池db-type: mysql # 使用mysql数据库driver-class-name: com.mysql.cj.jdbc.Driver # 驱动串url: jdbc:mysql://192.168.40.77:3306/seata?serverTimezone=Asia/Shanghai # 连接串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 # 超时时间
- 创建并运行 Seata 容器:
# 创建并运行Seata容器
# 参数: `-e SEATA_IP=192.168.40.77`: 指定Seata容器IP
# 参数: `-e SEATA_PORT=8091`: 指定Seata容器端口
docker run -d --name seata --network my-net \-p 8091:8091 -p 7091:7091 \-e SEATA_IP=192.168.40.77 \-e SEATA_PORT=8091 \-v /opt/seata/conf/application.yml:/seata-server/resources/application.yml \registry.cn-hangzhou.aliyuncs.com/joezhou/seata:latest# 查看Seata容器
docker ps --format "{{.ID}}\t{{.Names}}\t{{.Ports}}"
docker logs seata --tail 30# 永久开放服务端8091和客户端7091端口
firewall-cmd --add-port=8091/tcp --permanent
firewall-cmd --add-port=7091/tcp --permanent
firewall-cmd --reload
- 在 Windows 中访问 Seata 管控台 http://192.168.40.77:7091:使用 admin/admin 登录。
- 在 Nacos 管控台的服务列表中查看,是否成功注册了一个名为 seata-server 的服务。
S02. Seata项目整合
E01. 开发通用服务
武技:创建 seata-common 子项目并开发订单和商品对应的实体类。
- 开发订单实体类:
package com.joezhou.entity;/** @author 周航宇 */
@Data
public class Order implements Serializable {/** 主键 */private Long id;/** 订单编号 */private String sn;/** 购买商品的ID */private Long productId;/** 购买商品的数量 */private Integer number;
}
- 开发商品实体类:
package com.joezhou.entity;/** @author 周航宇 */
@Data
public class Product implements Serializable {/** 主键 */private Long id;/** 商品标题 */private String title;/** 商品库存量 */private Long stock;
}
E02. 开发商品服务
武技:创建 seata-product 子项目
- 添加三方依赖:
<dependencies><!--seata-common--><dependency><groupId>com.joezhou</groupId><artifactId>seata-common</artifactId><version>1.0-SNAPSHOT</version></dependency><!--mysql-connector-j--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>${mysql-connector-j.version}</version><scope>runtime</scope></dependency><!--mybatis-spring-boot-starter--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis-spring-boot-starter.version}</version></dependency><!--spring-cloud-starter-alibaba-nacos-discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--spring-cloud-starter-openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--spring-cloud-loadbalancer--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId></dependency><!--spring-cloud-starter-alibaba-seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><!--spring-boot-starter-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring-boot-starter-test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency>
</dependencies>
- 开发主配文件:
server:port: 14801 # 端口号
spring:application:name: seata-product # 项目名称 datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.40.77:3306/micro_transaction?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: adminpassword: admincloud:nacos:discovery:server-addr: 192.168.40.77:8848 # nacos地址 mybatis:type-aliases-package: com.joezhou.entity # 实体类别名包扫描 configuration:map-underscore-to-camel-case: true # 下划线自动转驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL
- 开发启动类:
package com.joezhou;/** @author 周航宇 */
@EnableDiscoveryClient
@MapperScan("com.joezhou.mapper")
@EnableFeignClients(basePackages = "com.joezhou.feign")
@SpringBootApplication
public class SeataProductApp { public static void main(String[] args) { SpringApplication.run(SeataProductApp.class, args); }
}
1. 开发数据层 - 扣库存
武技:在 seata-product 子项目中开发扣减库存的数据层代码。
package com.joezhou.mapper;/** @author 周航宇 */
@Repository
public interface ProductMapper {/*** 根据主键查询商品信息** @param id 商品主键* @return 商品信息*/@Select("select `id`, `title`, `stock` from `product` where `id` = #{param1}")Product selectById(Long id);/*** 根据主键修改商品库存** @param product 商品实体* @return 影响条目数*/@Update("update `product` set stock = #{stock} where `id` = #{id}")int updateStock(Product product);
}
2. 开发业务层 - 扣库存
武技:在 seata-product 子项目中开发扣减库存的业务层代码。
- 开发业务接口:
package com.joezhou.service;/** @author 周航宇 */
public interface ProductService {/*** 扣减商品库存** @param productId 商品主键* @param number 扣减数量* @return 影响条目数*/int updateStock(Long productId, Integer number);
}
- 实现业务接口:
package com.joezhou.service.impl;/** @author 周航宇 */
@Service
public class ProductServiceImpl implements ProductService {@Autowiredprivate ProductMapper productMapper;@Overridepublic int updateStock(Long productId, Integer number) {// 根据主键查询商品信息Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException("商品不存在");}// 判断库存是否充足Long currentStock = product.getStock();if (currentStock < number) {throw new RuntimeException("库存不足");}// 更新库存product.setStock(currentStock - number);return productMapper.updateStock(product);}
}
3. 开发控制层 - 扣库存
武技:在 seata-product 子项目中开发扣减库存的控制层代码。
- 开发控制器:
package com.joezhou.controller;/** @author 周航宇 */
@RestController
@RequestMapping("/api/v1/product")
public class ProductController {@Autowiredprivate ProductService productService;@PostMapping("/updateStock/{productId}/{number}")public int updateStock(@PathVariable("productId") Long productId, @PathVariable("number") Integer number) throws TransactionException {return productService.updateStock(productId, number);}
}
- 测试控制器:
### 扣减库存
POST http://localhost:14801/api/v1/product/updateStock/3/2
Content-Type: application/x-www-form-urlencoded
E03. 开发订单服务
武技:创建 seata-order 子项目
- 添加三方依赖:
<dependencies><!--seata-common--><dependency><groupId>com.joezhou</groupId><artifactId>seata-common</artifactId><version>1.0-SNAPSHOT</version></dependency><!--mysql-connector-j--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>${mysql-connector-j.version}</version><scope>runtime</scope></dependency><!--mybatis-spring-boot-starter--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis-spring-boot-starter.version}</version></dependency><!--spring-cloud-starter-alibaba-nacos-discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--spring-cloud-starter-openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--spring-cloud-loadbalancer--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId></dependency><!--spring-cloud-starter-alibaba-seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><!--spring-boot-starter-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring-boot-starter-test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency>
</dependencies>
- 开发主配文件:
server: port: 14802 # 端口号
spring: application: name: seata-order # 项目名称 datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.40.77:3306/micro_transaction?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: adminpassword: admin cloud: nacos: discovery: server-addr: 192.168.40.77:8848 # nacos地址 mybatis: type-aliases-package: com.joezhou.entity # 实体类别名包扫描 configuration: map-underscore-to-camel-case: true # 下划线自动转驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL
- 开发启动类:
package com.joezhou;/** @author 周航宇 */
@EnableDiscoveryClient
@MapperScan("com.joezhou.mapper")
@EnableFeignClients(basePackages = "com.joezhou.feign")
@SpringBootApplication
public class SeataOrderApp { public static void main(String[] args) { SpringApplication.run(SeataOrderApp.class, args); }
}
1. 开发远程接口
武技:在 seata-order 子项目中开发远程接口。
package com.joezhou.feign;/** @author 周航宇 */
@FeignClient("seata-product")
public interface ProductFeign {@PostMapping("/api/v1/product/updateStock/{productId}/{number}")int updateStock(@PathVariable("productId") Long productId,@PathVariable("number") Integer number);
}
2. 开发数据层 - 下订单
武技:在 seata-order 子项目中开发添加订单的数据层代码。
package com.joezhou.mapper;/** @author 周航宇 */
@Component
public interface OrderMapper {/*** 下单购买商品** @param order 订单实体* @return 影响条目数*/@Options(useGeneratedKeys = true, keyProperty = "id")@Insert("""insert into `order` (`sn`, `product_id`, `number`)values (#{sn}, #{productId}, #{number})""")int insert(Order order);
}
3. 开发业务层 - 下订单
武技:在 seata-order 子项目中开发添加订单的业务层代码。
- 开发业务接口:
package com.joezhou.service;/** @author 周航宇 */
public interface OrderService {/*** 下单购买商品** @param order 订单实体* @return 影响条目数*/int insert(Order order);
}
- 实现业务接口:
package com.joezhou.service.impl;/** @author 周航宇 */
@Service
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate ProductFeign productFeign;@Transactional(rollbackFor = Exception.class)@Overridepublic int insert(Order order) {// 生成订单号 order.setSn(UUID.randomUUID().toString());// 添加订单表记录if (orderMapper.insert(order) <= 0) {throw new RuntimeException("订单记录添加失败");}// 远程调用扣减库存if (productFeign.updateStock(order.getProductId(), order.getNumber()) <= 0) {throw new RuntimeException("扣减库存失败");}return 1;}
}
4. 开发控制层 - 下订单
武技:在 seata-order 子项目中开发添加订单的控制层代码。
- 开发控制器:
package com.joezhou.controller;/** @author 周航宇 */
@RestController
@RequestMapping("/api/v1/order")
public class OrderController {@Autowiredprivate OrderService orderService;@PostMapping("/insert")public int insert(@RequestBody Order order) {return orderService.insert(order);}
}
- 测试控制器:
### 添加订单:库存不足时,仍然会添加订单记录,本地事务保护未生效
POST http://localhost:14802/api/v1/order/insert
Content-Type: application/json{"productId": 1,"number": 1
}
E04. 添加事务保护
1. 添加主配项
武技:分别在两个子项目中添加主配项
seata: enabled: true # 启用seata tx-service-group: default_tx_group # 事务服务组,对应配置中心中,以 `service.vgroupMapping` 为前缀的那个文件registry: type: nacos # 使用Nacos注册中心 nacos: application: seata-server # Seata在Nacos中的服务名 server-addr: 192.168.40.77:8848 # 注册中心服务地址 namespace: seata-namespace # 注册中心命名空间 cluster: default # 注册中心集群名称 group: SEATA_GROUP # 注册中心分组名称config: type: nacos # 使用NacosConfig配置中心 nacos: server-addr: 192.168.40.77:8848 # 配置中心服务地址 namespace: seata-namespace # 配置中心命名空间 data-id: seataServer.properties # 配置中心配置文件
2. 升级业务层 - 扣库存
心法:seata 使用
GlobalTransactionContext.reload(RootContext.getXID()).rollback()
进行全局回滚,注意该方法需要抛出 TransactionException 异常,千万不要使用 try-catch 捕获该异常。
武技:在 seata-product 子项目中升级扣减库存的业务方法
package com.joezhou.service.impl;/** @author 周航宇 */
@Service
public class ProductServiceImpl implements ProductService {@Overridepublic int updateStock(Long productId, Integer number) throws TransactionException {// 根据主键查询商品信息Product product = productMapper.selectById(productId);if (product == null) {// seata 全局回滚GlobalTransactionContext.reload(RootContext.getXID()).rollback();throw new RuntimeException("商品不存在");}// 判断库存是否充足Long currentStock = product.getStock();if (currentStock < number) {// seata 全局回滚GlobalTransactionContext.reload(RootContext.getXID()).rollback();throw new RuntimeException("库存不足");}// 更新库存product.setStock(currentStock - number);return productMapper.updateStock(product);}
}
3. 升级业务层 - 下订单
心法:seata 使用
@GlobalTransactional(rollbackFor = Exception.class)
注解开启全局事务,该注解可以和本地事务注解共存。
武技:在 seata-order 子项目中升级添加订单的业务方法
package com.joezhou.service.impl;/** @author 周航宇 */
@Service
public class OrderServiceImpl implements OrderService {@GlobalTransactional(rollbackFor = Exception.class)@Transactional(rollbackFor = Exception.class)@Overridepublic int insert(Order order) {// 添加订单表记录if (orderMapper.insert(order) <= 0) {throw new RuntimeException("订单记录添加失败");}// 远程调用扣减库存if (productFeign.updateStock(order.getProductId(), order.getNumber()) <= 0) {throw new RuntimeException("扣减库存失败");}return 1;}
}
4. 测试事务保护效果
### 添加订单:库存不足时,不会添加订单记录,seata 事务保护生效
POST http://localhost:14802/api/v1/order/insert
Content-Type: application/json{"productId": 1,"number": 1
}
Java道经第4卷 - 第8阶 - 事务机制