随着分布式的兴起,出现了一个问题:目前的数据库都只支持本地数据库事务的ACID特性,面对跨多个数据源时,容易出现“脏读”,回滚不成功等一些问题。Seata正是一种保障分布式事务一致性的解决方案。
目录
3.2 seata_order数据库中的t_order表结构
3.3 seata_storage数据库中的t_storage表结构
3.4 seata_account数据库中的t_account表结构
一. 简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。官方文档
1.1 特色功能
● 微服务框架支持:目前已支持Dubbo、Spring Cloud、Sofa-RPC、Motan 和 gRPC 等RPC框架,其他框架持续集成中
● AT模式:提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB、MariaDB、DaMeng、PolarDB-X 2.0、SQLServer。DB2开发中
● TCC模式:支持 TCC 模式并可与 AT 混用,灵活度更高
● SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式(开发中)
● XA 模式:支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle和MariaDB
● 高可用:支持存算分离的集群模式,计算节点可水平扩展,存储支持数据库和 Redis。Raft集群模式已进入beta验证阶段
1.2 架构图
1.3 解决方案
1.4 概念介绍
● TC(Transaction Coordinator)事务协调器:Seata本身,全局唯一。负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
● TM(Transaction Manager)事务管理器:标注全局@GlobalTransactional启动入口动作的微服务模块,是事务的发起者,全局唯一。负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开启事务、提交事务、回滚事务的决议
● RM(Resource Manager)资源管理器:数据库本身,全局可以有多个,并且一个微服务可以既是TM又是RM。负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态、驱动分支事务的提交和回滚。
● xid 全局事务ID:用于唯一标识一个分布式全局事务,随着任务链传递。xid是分布式事务管理的核心,seata通过xid保证事务的提交和回滚,确保数据的一致性和完整性。
1.5 工作流程
1.TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
2.XID 在微服务调用链路的上下文中传播
3.RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖
4. TM 向 TC 发起针对 XID 的全局提交或回滚决议
5.TC调度XID下管辖的全部分支事务完成提交或回滚请求
2. Sentinel的下载与安装
在使用Sentinel前请先确保可以进行Nacos和OpenFeign的基本使用,不会的先看我之前的文章
下载完成,并解压后进入conf目录下,推荐先备份一份系统默认的application.yml
修改application.yml文件,进行个性化配置(连接nacos,连接数据库)
# Copyright 1999-2019 Seata.io Group.
#
# Licensed 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: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${log.home:${user.home}/logs/seata}
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: 你的SEATA名字
password: 你的SEATA密码
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
username: 你的NACOS名字
password: 你的NACOS密码
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
namespace:
cluster: default
username: 你的NACOS名字
password: 你的NACOS密码
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver #mysql8的驱动,如果是8以下记得去掉cj
url: jdbc:mysql://localhost:3306/你的数据库名字?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: 你的Mysql用户
password: 你的Mysql密码
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 #用于生成jwt密钥
tokenValidityInMilliseconds: 1800000 #有效期
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/** #静态资源放行
配置完成并保存后先启动Nacos
启动Nacos完成后,进入seata的bin目录下,点击或cmd窗口运行seata-server.bat命令
尝试访问localhost:8848和localhost:7091
出现如上页面代表Seata启动成功并注册进Nacos了,默认用户名和密码都是seata
最后seata需要在数据库里建表,执行以下sql语句即可
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE DATABASE seata;
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;
-- 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);
3. 项目架构
作为入门篇,本文仅讲解AT模式与Mysql的整合
本文中将以下图为项目架构图,实现一个分布式交易项目来演示Seata的使用方案与作用。
3.1 数据库准备
应用seata AT模式的所有数据库中必须准备一张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;
3.2 seata_order数据库中的t_order表结构
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`count` int DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8
3.3 seata_storage数据库中的t_storage表结构
CREATE TABLE `t_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`total` int DEFAULT NULL COMMENT '总库存',
`used` int DEFAULT NULL COMMENT '已用库存',
`residue` int DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
3.4 seata_account数据库中的t_account表结构
CREATE TABLE `t_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
4. 创建工程
4.1 父工程依赖管理
负责管理所有子项目的默认版本依赖
请一定注意版本!SpringCloud容易和Seata出现版本不兼容的情况
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.haishi</groupId>
<artifactId>NacosTest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>commons</module>
<module>seata-account-service2003</module>
<module>seata-order-service2001</module>
<module>seata-storage-service2002</module>
</modules>
<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.0.2</spring-boot.version>
<spring.cloud.alibaba.version>2023.0.1.2</spring.cloud.alibaba.version>
<lombok.version>1.18.26</lombok.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.2</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<mapper.version>4.2.3</mapper.version>
<spring.cloud.version>2022.0.4</spring.cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2022.0.0.0-RC2</version>
</dependency>
<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>
<!--SpringBoot集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--通用Mapper4之tk.mybatis-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--springcloud 2023.0.0 包含了OpenFeign等版本管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4.2 子项目依赖,application.yml,主启动
由于三个微服务的项目依赖,application.yml,主启动近乎一模一样,这里就仅介绍一组了
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.haishi</groupId>
<artifactId>NacosTest</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>seata-storage-service2002</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- cloud_commons_utils-->
<dependency>
<groupId>com.haishi</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
application.yml(注意修改端口和数据库名称)
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: root
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
主启动
@SpringBootApplication
@MapperScan("com.haishi.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataStorageMainApp2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageMainApp2002.class,args);
}
}
4.3 工具模块
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.haishi</groupId>
<artifactId>NacosTest</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>commons</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.7</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
定义一个全局异常处理器
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 默认全局异常处理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e) {
log.error("全局异常信息exception:{}", e.getMessage(), e);
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),e.getMessage());
}
}
创建account模块的OpenFeign远程调用接口
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
//seata-account-service2003微服务中的业务方法,可自行实现
//扣减账户余额
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
创建storage模块的OpenFeign远程调用接口
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
//seata-storage-service2003微服务中的业务方法,可自行实现
//扣减库存
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
由于order模块既是RM又是TM,因此并不需要远程调用接口
4.4 seata-order-service模块
在OrderServiceImpl中实现业务逻辑,开启事务,实现订单流程。
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
//订单微服务通过OpenFeign去调用账户微服务
@Resource
private AccountFeignApi accountFeignApi;
//订单微服务通过OpenFeign去调用库存微服务
@Resource
private StorageFeignApi storageFeignApi;
//name:事务名称 rollbackFor:发生该异常或其子类,进行回滚
//@GlobalTransactional代表开启AT模式
@GlobalTransactional(name = "create-order",rollbackFor = Exception.class) //AT
@Override
public void create(Order order) {
//xid全局事务id 重要!!
String xid = RootContext.getXID();
//1. 新建订单
log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
//订单状态status:0:创建中;1:已完结
order.setStatus(0);
int result=orderMapper.insertSelective(order);
//插入订单成功后获得插入mysql的实体对象
Order orderFromDB=null;
if(result>0){
orderFromDB=orderMapper.selectOne(order);
log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getCount());
log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
System.out.println();
//3. 扣减账号余额
log.info("-------> 订单微服务开始调用Account账号,做扣减money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
System.out.println();
//4. 修改订单状态
log.info("-------> 修改订单状态");
orderFromDB.setStatus(1);
Example whereConditin = new Example(Order.class);
Example.Criteria criteria = whereConditin.createCriteria();
//将当前用户下所有账单完结
criteria.andEqualTo("userId", orderFromDB.getUserId());
criteria.andEqualTo("status", 0);
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereConditin);
//这里可以再根据完结的账单数量进一步处理,我这里就直接抛出异常了
if(updateResult>1){
throw new RuntimeException("有订单未完成,无法结账");
}
}
log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
}
}
controller层
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
4.5 测试
测试成功!
5.分布式事务详解(AT模式)
5.1 @GlobalTransactional
@GlobalTransactional用于开启AT模式,但是如果没有这个注解会发生什么?换句话说,不使用seata这个分布式事务解决方案,仅仅使用@Service自带的@Transactional会发生什么?
首先让我们把@GlobalTransactional注解注释掉
修改AccountServiceImpl,制造异常
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣减账户余额开始");
accountMapper.decrease(userId,money);
myTimeOut();
log.info("------->account-service中扣减账户余额结束");
}
//模拟超时异常,全局事务回滚
private static void myTimeOut()
{
try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
然后重启所有微服务
我们首先查看一下库存表
账户表
接着创建订单,返回超时异常
查看订单表:没有创建成功
但是account表和storage表却扣款成功
这说明普通的@Transactional在跨数据源的分布式事务中无法保证事务的原子性,只能做到对服务发起者所控制的单个数据源的回滚。因此,在以上情况中,订单模块出现异常,想要回滚事务,却只能回滚订单数据库的数据,对于仓库模块和账户模块,当程序执行完他们的service方法时,仓库表和账户表的数据就已经提交,无法回滚。
这是由于@Transactional本身是基于数据库start transaction;rollback ;等指令实现的,而目前的数据库都还不支持分布式事务......也许有一天出现一款稳定的分布式数据库我们就没必要学seata了呢。
然后让我们开启@GlobalTransactional注解再试一下
回滚成功
5.2 整体机制
明明程序在走出仓库和账户模块的service方法时,便已经向数据库提交本地事务,seata又是如何做到对业务无侵入地完成回滚分布式事务的呢?
TM会首先向TC申请一个xid,开启全局事务,尝试将xid随调用链传递,将所有RM注册进TC,对于所有RM本地事务,执行以下操作:
官网中将其分为了两阶段提交协议的演变:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
在一阶段中,Seata会拦截业务SQL
1. 解析SQL语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成“before image”
2.执行业务SQL更新数据
3.将新快照保存成“after image”
4.生成行锁(seata的行锁,非数据库行锁)
以上操作全部在一个数据库事务内完成,保证了一阶段操作的原子性
不过需要注意的是,此处生成的行锁是seata的行锁,也即全局锁,用于在分布式事务中实现读隔离和写隔离。全局锁是由seata管理的一个逻辑锁,只有使用seata的微服务能获取或者感知到这把锁。
● 每个分布式事务在一阶段本地事务提交前,必须确保拿到全局锁。
● 拿不到全局锁则无法提交本地事务
● 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁
比如在上述更新表数据中,有两个线程同时想要修改某一行的数据,tx1先获取到了行锁,tx2只有当TX1释放行锁后,TX2才能获取行锁对该行进行修改。
写隔离:
当然Seata还支持读隔离,seata会对想要读取的数据加上行锁。
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
二阶段则会分两种情况
(1)正常提交
因为业务 SQL在一阶段已经提交至数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删除,完成数据清理即可。
(2)异常回滚
如果发生异常,seata就需要回滚一阶段本地数据库已经提交的数据
回滚方式是运用“before image”快照恢复原数据;但在还原前首先需要校验脏写,对比数据库当前业务数据和“after image”
若两份数据一样,就说明没有脏写,开始还原数据。如果不一致就说明发生脏写,需要转人工或编写额外机制处理。
正常项目架构使用seata框架不会发生脏写问题,只有当seata校验事务的时候,另一个不受seata管理的服务对数据库进行数据修改,或者进行到一半服务器停电等特殊情况会发生该类问题。
5.3 @GlobalLock
我们需要去思考一种情况:除了创建订单的服务调用链以外,我们还需要新增一个修改库存价格的功能。库存表仅仅是对库存数据库本地事务的修改,但是如果不注册全局事务,则有可能发生脏写问题。
注册全局事务无疑是一个重量级操作,那么有没有办法去优化它的效率?
@GlobalLock提供了对于分布式事务的优化:@GlobalLock并不会像@GlobalTransactional那样去向seata申请一个xid开启全局事务,尝试将xid随调用链传递,它仅仅会获取seata中需要修改或访问数据的全局锁,如果获取不到,则说明当前有事务正在执行,可以重试获取。
注意事项:
在使用@GlobalLock注解的时候,我们需要更新之前,在查询方法中添加排它锁
SELECT * FROM t_storage WHERE product_id=#{id} FOR UPDATE
只有在 SQL 中添加了 FOR UPDATE
,Seata 才会对数据库行加上排他锁,确保在事务失败时本地锁能及时释放,允许全局事务回滚或重试。如果不添加 FOR UPDATE
,Seata 可能无法及时释放本地锁,导致全局事务在回滚时无法立即获取全局锁,从而造成长时间等待,直到 @GlobalLock
所在的事务超时失败才能拿到本地锁并释放全局锁,影响系统的并发性。
5.4 回滚原理
那么具体seata是如何控制已经提交的本地事务进行回滚的呢?
相信很多熟悉MVCC的小伙伴在见到undo_log这个表名的时候便已经有所猜测。没错!seata正是效仿了MVCC的事务机制完成分布式事务ACID特性的保障 ,利用开启事务前后数据的全量快照实现。
在执行事务的时候,TM会向TC申请开启全局事务,获取xid,并随任务链传递将任务链全部注册进TC。
如前面所言,seata在一阶段会保存一份“before image”和 "after image"快照,成功获取行锁提交本地事务前,还会将一条日志插入undo_log中,用于后续全局事务的管理
除去branch_id,xid等全局事务标识外,rollback_info中存储了"before image"和"after image"等信息,用于辅助全局事务的提交或回滚。
内部信息格式大致是这样:
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
当TM发起全局事务回滚的请求时,seata就能通过"before image"回复数据的原始状态
或者当TM发起全局事务提交的请求时,seata就能校验“after image”检测是否发生脏写
6. 总结
Seata 是一个开源的分布式事务解决方案,旨在解决微服务架构下分布式系统中的事务一致性问题。它通过提供全局事务的管理能力,帮助开发者确保多个分布式服务调用的原子性和一致性。以下是 Seata 的主要功能:
● 分布式事务管理
● 全局事务的生命周期管理
● AT 模式
● 全局锁管理
● 扩展性和可插拔设计
● 支持多种数据库和中间件
完整的代码放在了仓库中,可自行获取:wandering_4/Spring Cloud Alibaba (gitee.com)
💞 💞 💞本文也就到此结束了,希望能给大家带来帮助
最近也是来到了秋招的好日子,祝大家春风得意马蹄,斩获自己想要的offer