分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
我们知道本地事务依赖数据库本身提供的事务特性来实现,但是在分布式环境下,会变成下边这样:
@Transactional
@Override
public void insertOrder(TbOrder tbOrder) {
//1、保存订单
tbOrderMapper.insertSelective(tbOrder);
//2、扣减库存
itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());
//模拟扣款失败,此时扣减库存会回滚吗?
int a = 6/0;
}
还可以设想,当远程调用扣减库存成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了保存订单的操作,此时也不满足一致性。
因此在分布式架构的基础上,传统数据库事务就无法使用了,订单和库存不在一个数据库中甚至不在一个应用系统里,实现业务需要通过远程调用,由于网络问题就会导致分布式事务问题。
分布式事物产生的场景
1、典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和商品微服务,下单的同时订单微服务请求商品微服务减库存。简言之:跨JVM进程产生分布式事务。
2、单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。
3、多服务访问同一个数据库实例 比如:订单微服务和商品微服务即使访问同一个数据库也会产生分布式事务,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。简言之:跨JVM进程产生分布式事务。
分布式事务解决方案之seata
Seata是什么
2019年1月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),其愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。后来更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套分布式事务解决方案。
官网:http://seata.io/zh-cn/
Seata的分布式事务解决方案
Seata提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入.
TCC模式:最终一致的分阶段事务模式,有业务侵入
AT(auto transaction)模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式.
SAGA模式:长事务模式,有业务侵入
Seata的核心组件
Seata事物管理中有三个重要的核心组件:
- TC(Transaction Coordinator)-事务协调者:维护分支事务的状态,协调全局事务提交或回滚。
- TM(Transaction Manager)-事务管理器:定义全局事务的范围,并开始全局事务。
- RM(Resource Manager)-资源管理器:向TC注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
seata安装配置
下载地址: https://github.com/seata/seata/releases
上传并解压安装包
cd /usr/upload
#解压到 /usr/local
tar -zxvf seata-server-1.4.2.tar.gz -C /usr/local
修改配置文件
修改seata/seata-server-1.4.2/conf/
目录下的registry.conf
文件:
这里注册中心使用的是nacos,多余的都删除了
registry {
# tc服务的注册中心类型,这里选择nacos,也可以是eureka、zookeeper等
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
# seata tc服务注册到nacos的服务名称,可以自定义
application = "seata-server"
# nacos的地址
serverAddr = "192.168.226.203:8848"
# seata服务所在分组
group = "DEFAULT_GROUP"
# seata服务所在的名称空间,这里不填就是使用默认的"public"
namespace = ""
# TC集群名,默认是"default"
cluster = "default"
# 这个是nacos的用户名 默认不写
username = ""
# 这个是nacos的密码 默认不写
password = ""
}
}
# 集群配置
config {
# tc服务的配置中心类型:file、nacos 、apollo、zk、consul、etcd3
# file、nacos 、apollo、zk、consul、etcd3
type = "naocs"
nacos {
serverAddr = "192.168.226.203:8848"
namespace = ""
group = "DEFAULT_GROUP"
username = ""
password = ""
dataId = "seataServer.properties"
}
}
在Nacos中添加配置信息
这里名称与集群配置名称保持一致
配置信息地址:https://gitee.com/seata-io/seata/blob/develop/script/config-center/config.txt
数据库信息需要修改
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.226.201:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=1111
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
创建TC服务数据表
建表
建表语句地址: https://gitee.com/seata-io/seata/blob/develop/script/server/db/mysql.sql
-- -------------- 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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
启动Seata
cd /usr/local/seata/seata-server-1.4.2/bin/
# 启动
./seata-server.sh
# 或指定ip端口
./seata-server.sh -h 192.168.226.209 -p 8091
进入nacos注册中心
点击详情进入
ip不一致,重新启动,指定ip端口
./seata-server.sh -h 192.168.226.209 -p 8091
再次查看nacos
seata演示分布式事务
分布式事务的实现测试,orderService远程调用itemService扣减库存
pojo类,mapper使用逆向工程生成,方法较多,只使用到一两个方法实验
表结构
undo_log建表语句地址:https://gitee.com/seata-io/seata/blob/develop/script/client/at/db/mysql.sql
item库
-- 商品表 ----------------------------
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`num` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- Records of tb_item ----------------------------
INSERT INTO `tb_item` VALUES ('1', '手机', '100');
INSERT INTO `tb_item` VALUES ('3', '电脑', '100');
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
order库
-- Table structure for tb_order ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`item_id` int(11) DEFAULT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
工程结构
seata_demo
pom
<?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.seatademo</groupId>
<artifactId>seata_demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>common_pojo</module>
<module>seata_item_service</module>
<module>seata_order_service</module>
<module>seata_item_feign</module>
</modules>
<properties>
<!-- 项目编译JDK版本 -->
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring-boot-version>2.3.2.RELEASE</spring-boot-version>
<spring-cloud-version>Hoxton.SR9</spring-cloud-version>
<spring-cloud-alibaba-version>2.2.6.RELEASE</spring-cloud-alibaba-version>
<mybatis-version>3.5.1</mybatis-version>
<mysql-connector-java-version>5.1.38</mysql-connector-java-version>
<druid-version>1.0.9</druid-version>
<spring-mybatis-version>2.0.1</spring-mybatis-version>
</properties>
<dependencyManagement>
<dependencies>
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Netflix-->
<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 阿里巴巴-->
<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>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis-version}</version>
</dependency>
<!-- MySql Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java-version}</version>
</dependency>
<!--Alibaba DataBase Connection Pool-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid-version}</version>
</dependency>
<!--MyBatis And Spring Integration Starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${spring-mybatis-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
<!-- mapper.xml文件在java目录下 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<!--解决maven项目resources目录显示为普通目录问题-->
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
common_pojo
pom
<?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">
<parent>
<artifactId>seata_demo</artifactId>
<groupId>com.seatademo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common_pojo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
pojo
package com.seatademo.pojo;
public class TbItem {
private Integer id;
private String name;
private Integer num;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
package com.seatademo.pojo;
import java.util.ArrayList;
import java.util.List;
public class TbItemExample {
<