整个项目请看gitee:https://gitee.com/xwb1056481167/spring-cloud
nacos的配置安装请到:https://blog.csdn.net/www1056481167/article/details/113612177查看
seata官网:https://seata.io/zh-cn
分布式事务的处理过程为
一ID+三组件模型
核心概念
- 一ID
全局事务的id - 三组件概念
- 1、TC(Transaction Coordinator)-事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚 - 2、TM(Transaction Manager)-事务管理者
定义全局事务的范围:开始全局事务、提交或回滚全局事务 - 3、RM(Resource Manager)-资源管理器
管理分支事物处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
- 1、TC(Transaction Coordinator)-事务协调者
处理流程图
说明:
1、TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
2、XID在微服务传播链路上下文中传播
3、RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
4、TM向TC发起针对XID的全局提交或回滚决议
5、TC调度XID下管辖的全部分支事务完成提交或或回滚请求
seata的环境搭建配置
一、seata的下载
下载地址:https://github.com/seata/seata/releases
版本 0.9: windows: https://github.com/seata/seata/releases/download/v0.9.0/seata-server-0.9.0.zip
二、seata的安装
1、修改file.conf自定义事务组名称+事务日志模式为db+数据库连接
1、service的vgroup_mapping.my_test_tx_group属性
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "fsp_tx_group"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
2、指定存储方式为数据库
store {
## store mode: file、db
mode = "db"
...
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
3、创建数据库seata,并且创建表
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
2、修改registry.conf注册信息
修改注册类型为nacos,并且指定nacos的注册地址为localhost:8848(根据自己的nacos配置)
有关nacos的安装配置请到https://blog.csdn.net/www1056481167/article/details/113612177查看
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
...
启动验证是否成功配置
1、启动nacos
2、启动seata(双击seata-server.bat找到注册进nacos的注册语句即可)
三、准备业务表结构
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
`total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度',
`used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用额度',
`residue` decimal(10, 0) NULL DEFAULT NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4;
DROP TABLE IF EXISTS `seata_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',
`total` int(11) NULL DEFAULT NULL COMMENT '库存',
`used` int(11) NULL DEFAULT NULL COMMENT '已用库存',
`residue` int(11) NULL DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 ;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',
`count` int(11) NULL DEFAULT NULL COMMENT '数量',
`money` decimal(11, 0) NULL DEFAULT NULL COMMENT '金额',
`status` int(1) NULL DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4;
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最终表结构
新建订单、库存、余额模块
业务需求:下订单->减库存->扣余额->改(订单)状态
以下只列举了核心代码,全部代码请到gitee上查看
新建订单模块seata-order-service2001
1、pom.xml(部分省略)
<!-- SpringCloud alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除自身附带的jar包 -->
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入和服务端相匹配的jar包 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery: #Nacos注册中心地址
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
# type: com.alibaba.druid.pool.DruidDataSource
feign:
hystrix:
enabled: true
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
3、备份seata的配置文件file.conf registry.conf
4、提供dao,domain,mapper*.xml,controller。
5、service 特别注意的是调用的其他微服务的方法,需要@FeignClient(value="xx")标明调用的方法接口,本身的service提供的方法,不用该注解
6、config的配置
//1、mybatis扫描的包
@Configuration
@MapperScan("org.xwb.springcloud.dao.*")
public class MyBatisConfig {
}
//2、druid数据源
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources(mapperLocations));
SqlSessionFactory factory;
try {
factory = bean.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
return factory;
}
}
7、主启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
特别注意的是 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
排除自身的数据源,使用自己定义的数据源
新建库存模块seata-storage-service2002
同以上2001相同,以下只说明不同之处
1、application.xml
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery: #Nacos注册中心地址
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: fsp_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
# type: com.alibaba.druid.pool.DruidDataSource
feign:
hystrix:
enabled: true
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
其他业务可直接查看项目
新建账户模块seata-account-service2003
同2003相同 ,以下只列出不同之处
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
# type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
feign:
hystrix:
enabled: true
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath*:mapper/*.xml
其他业务参照代码原始项目请到gitee上查看
测试(正常测试、GlobalTransactional)
正常测试(不加GlobalTransactional统一事务进行测试)
1、启动nacos、seata、2001、2002、2003
2、浏览器输入 http://localhost2001/order/create?userId=1&productId=1&count=1&money=100查看库存是正确的
3、此时需要将对seata-account-service2003账户付款进行改动(修改为超时,账户扣款失败)
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("********->account-service中扣减账户余额开始");
//此处故意超时
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
log.info("********->account-service中扣减账户余额结束");
}
}
测试结果
会发现,成功创建了订单,但是订单的状态却不对。
分析得知:
整个事物分为4部分1、新建订单2、扣减库存3、扣减账户4、修改订单状态
前面3部分都执行成功,但是最后需要修改订单的状态确失败,由于我们设置了超时,故意让等待超时处理,此时由于没有添加全局事务的控制,导致中途失败的无法执行成功,但是在他之前
执行成功的无法回滚,就导致了整个事物不正确。订单下了,扣款,修改订单状态却不对。此时就需要全局事务来解决事物不统一的问题。
使用GlobalTransactional
通过以上测试会发现事物处理不统一接下来使用seata的全局事务进行控制
1、添加注解@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)name自定义即可
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
log.info("--------->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("------------->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("------------->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("------------->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("------------->订单微服务开始调用账户,做扣减end");
//4 修改订单状态
log.info("------------->修改订单状态开始");
orderDao.update(order.getUserId(), 0);
log.info("------------->修改订单状态结束");
log.info("------------->下订单结束了");
}
重启应用,再次请求订单,会发现订单整个订单都添加不进来,全部回滚
原理解析
TM开启分布式事务(TM向TC注册全局事务记录)
按照业务场景,编排数据库、服务器等事务内资源(RM向TC汇报资源准备状态
TM结束分布式事务,事务一阶段准备结束(TM通知TC提交、回滚分布式事务)
TC汇报事务信息,决定分布式事务是提交还是回滚
TC通知所有RM提交回滚资源,事务二阶段结束
核心原理分析
一阶段加载
在一阶段,Seata会拦截"业务SQL"
1、解析sql语义,找到"业务SQL"要更新的业务数据,在业务数据被更新前,将其保存到"before image",
2、执行"业务SQL"更新业务数据,在业务数据更新之后。
3、将其保存城"after image",最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段(成功、失败)
1、二阶段如果顺利提交的话。
因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行级锁,完成数据清理即可。
2、二阶段失败(回滚)
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的"业务SQL"还原业务数据
回滚方式便是用"before image" 还原业务数据;但在还原前要首先要校验脏写,对比"数据库当前业务数据"和"after image", 如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写需要转人工处理。