# 分布式事务
# 事务
- 指要做的或所做的事情
- 事务由事务开始(begin transaction)和事务结束(end transaction) 之间执行的全体操作组成;
- 事务中的多个操作,要么全部成功,要么全部失败;
# 分布式事务
- 微服务环境下,一个事务由多个分布式应用构成
- 事务操作位于不同的节点上,原本通过数据库连接控制的单体应用事务不再适用
# 常见分布式事务处理模型
# 两阶段提交(Two-phase Commit,2PC)
引入协调者(Coordinator),来协调参与者的行为,协调者最终决定这些参与者是否要真正执行事务
- 第一阶段:准备阶段
- 协调者询问事务参与者,是否执行成功
- 事务参与者发回事务执行结果
- 第二阶段:提交阶段
- 如果所有事务参与者都有成功反馈,则通知所有事务参与者提交事务;
- 否则,协调者发送通知,让所有的事务参与者回滚事务
- 存在的问题
- 事务参与者在等待其它参与者响应时,处于同步阻塞状态
- 二阶段,如果网络异常,会导致部分Commit事务,使得数据不一致,不可靠;
- 任意一个节点失败,就会导致事务崩溃,容错性差;
# 补偿事务(Try-Confirm-Cancel,TCC)
针对每个操作,都注册一个与其对应的确认和补偿操作.分为三个阶段.
- 第一阶段:Try
- 对业务系统做检测,及资源预留(上锁).
- 第二阶段:Confirm
- 对业务系统做确认提交。
- 第三阶段:Cancel
- 在业务执行错误时,需要回滚的状态下,执行的业务取消,释放预留资源
- 存在的问题
- 属于应用层的一种补偿方式,需要程序员在实现时,多写很多补偿代码;
- 在一些场景中,一些业务流程用TCC不太好定义及处理;
- 第二阶段,跟第三阶段都可能失败,不稳定;
# 本地消息表(异步确保)
新增本地消息表,它与业务数据表,处于同一个数据库中,利用本地事务,来保证对这两个表的操作满足事务特性。
# MQ事务消息
有一些第三方MQ支持事务消息,如RocketMQ.支持事务消息的方式,类似于二阶段提交.
# seata
一款由阿里巴巴开源的分布式事务解决方案,致力于在微服务架构下,提供高性能和简单易用的分布式事务服务。
# 发展历程
原名为fescar(Fast&Easy Commit And Rollback),由2014年的**TXC(Taobao Transaction Constructor)及2016年的GTS(Global Transaction Service)**的技术积累而来。
2019.01,发布0.1版本
2019.08,发布0.8版本
2020.02,发布1.1版本
2021.04,发布1.4.2版本
# 事务模型
- TM: 控制全局事务的边界,全局事务的开启、提交、回滚
- RM: 控制分支事务,分支事务的注册、状态汇报、接受事务协调器指令,分支事务的提交和回滚
- TC:事务协调器,维护全局事务的状态,负责协调并驱动全局事务的提交或回滚
# 事务模式
# AT模式
两阶段提交协议的演变。
- 一阶段: 业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段:
- 成功,则提交异步化
- 失败,则回滚。 通过一阶段的回滚日志,进行反向补偿
# TCC模式
- 支持把自定义的事务,纳入到全局事务的管理中。
# Saga模式
- 长事务解决方案
# Seata-AT模式实践
# 启动nacos作为注册中心
略
注:nacos1与nacos2有一些差别不兼容,建议选择nacos1.x
# 获取seata-server并修改配置(1.4.2)
其它版本不保证兼容,尤其是配置项等(目前是最新版,20220325)
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
file {
name = "file.conf"
}
}
- file.conf
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true"
user = "root"
password = "chenkaihai"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
# seata-server数据表说明
- 全局事务表global_table
- todo
- 分支事务表branch_table
- todo
- 全局锁lock_table
- todo
# 启动seata-server(TC)
- 创建库表结构
-- 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-server的lib目录中,将jdbc文件夹下的mysql驱动,拷贝至lib目录;
- 修改mysql连接参数
- 执行
seata-server.bat
# 启动客户端(TM,RM)
- 修改数据库连接参数,建表及初始化
SET
FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`amount` double(14, 2
) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(255) DEFAULT NULL,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
`amount` double(14, 2
) DEFAULT '0.00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_stock`;
CREATE TABLE `t_stock`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `undo_log`;
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;
SET
FOREIGN_KEY_CHECKS=1;
INSERT INTO `t_account`
VALUES ('1', '1', '4000.00');
INSERT INTO `t_stock`
VALUES ('1', 'C201901140001', '水杯', '1000');
- 在nacos配置中心,配置事务分组,几个应用的事务分组,值为default
seata:
tx-service-group: automannn-business-order # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
tx-service-group: automannn-business # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
tx-service-group: automannn-business-stock # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
seata:
tx-service-group: automannn-business-account # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
group=SEATA_GROUP
dataId=service.vgroupMapping.automannn-business,\
service.vgroupMapping.automannn-business-account,\
service.vgroupMapping.automannn-business-order,\
service.vgroupMapping.automannn-business-stock
content=default
- 分别启动客户端应用,建议顺序
- account
- stocka
- order
- business
# 调用接口检验
# 执行成功,全局事务提交
http://localhost:8104/business/buy
{
"userId": "1",
"commodityCode": "C201901140001",
"name": "成是党",
"count": 59,
"amount": 54
}
# 执行失败,分布式事务回滚
http://localhost:8104/business/buyFail
{
"userId": "1",
"commodityCode": "C201901140001",
"name": "成是党",
"count": 59,
"amount": 54
}
# 官方demo存在的问题
# dubbo版本的问题,导致消费者无法正常发现提供者
- 通过给服务设置分组,指定同一分组,进行处理;
# 客户端无法发现Seata-Server
- 在nacos注册中,配置事务分组集群
# 集成指导
# springBoot依赖项
<properties>
<seata.version>1.4.2</seata.version>
</properties>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>