一、什么是Seata?
1.基本简介
- Seata 是Alibaba开源的一款分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
- Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
2.它的由来
-
在传统的单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务来保证。
-
随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
-
在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,一次业务操作需要操作多个数据源或需要进行远程调用,就会产生分布式事务问题。
3.它的特点
- 对业务无侵入:能够减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
-
高性能:能够减少分布式事务解决方案所带来的性能消耗
二、相关概念
1.分布式事务的定义
我们可以把一个分布式事务理解成一个包含了若干分支事务的全局事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个满足ACID的本地事务。这是我们对分布式事务结构的基本认识,与 XA 是一致的。
2.分布式事务的核心组件
-
Transaction Coordinator (TC)
事务协调器,维护全局和分支事务的运行状态,负责协调并驱动全局事务的提交或回滚
-
Transaction Manager (TM)
控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
-
Resource Manager (RM)
控制分支事务处理的资源 ,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支事务的提交和回滚
3.分布式事务的处理过程
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
- XID 在微服务调用链路的上下文中传播
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖
- TM 向 TC 发起针对 XID 的全局提交或回滚决议
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求
三、四种事务模式
1.AT模式
-
基本简介
- AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务 SQL,便能轻松接入分布式事务,所以AT 模式是一种对业务无任何侵入的分布式事务解决方案。
- 阶段一:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 阶段二:非常快速地完成提交异步化或者回滚通过阶段一的回滚日志进行反向补偿。
-
执行流程
- TM端使用
@GlobalTransaction
进行全局事务开启、提交、回滚 - TM开始RPC调用远程服务
- RM端
seata-client
通过扩展DataSourceProxy
,实现自动生成UNDO_LOG
与TC
上报 - TM告知TC提交/回滚全局事务
- TC通知RM各自执行
commit/rollback
操作,同时清除undo_log
- TM端使用
-
执行详细步骤
-
阶段一
- TM向TC发起全局事务,生成XID(全局锁)
- 解析业务 SQL
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据
- 执行业务 SQL
- 查询后镜像:根据前镜像的结果,通过主键定位数据
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG表中
- 提交前,向 TC 注册分支
- 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交
- 将本地事务提交的结果上报给 TC
-
阶段二提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
-
阶段二回滚
- TM执行失败,通知TC全局回滚
- TC此时通知所有的RM进行回滚
- RM收到 TC 的分支回滚请求,开启一个本地事务
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录
- 拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,证明出现了脏写,这时就需要转人工处理。
- 如果没有出现脏写,则根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
-
-
TC事务协调端相关数据表
-
global_table
:全局事务每当有一个全局事务发起后,就会在该表中记录全局事务的ID -
branch_table
:分支事务记录每一个分支事务的ID,分支事务操作的哪个数据库等信息 -
lock_table
:记录全局锁 -
undo_log
:操作日志存储表
-
-
如何保证写隔离
- 一阶段本地事务提交前,需要确保先拿到全局锁 。
- 拿不到全局锁的事务 ,就不能提交本地事务。
- 拿到全局锁的事务如果尝试被限制在一定范围内,则超出范围将放弃并回滚本地事务,释放本地锁。
- tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900
- tx1 在本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁
- tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800
- tx2 在本地事务提交前,尝试拿该记录的全局锁,但是 tx1 在全局提交前,该记录的全局锁被 tx1 持有,所以 tx2 需要重试等待全局锁
- tx1 二阶段全局提交,释放全局锁,此时 tx2 拿到全局锁提交本地事务。
- 如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
- 此时,tx2 仍在等待该数据的全局锁,同时持有本地锁,所以 tx1 的分支回滚会失败。
- tx1 分支的回滚会一直重试,直到 tx2 的全局锁等待超时,那么 tx2 放弃全局锁并回滚本地事务释放本地锁,tx1 获取到本地锁分支回滚最终成功。
-
如何保证读隔离
- 数据库本地事务隔离级别是读已提交(Read Committed)或以上
- Seata(AT 模式)的全局事务默认隔离级别是读未提交(Read Uncommitted)
- SELECT FOR UPDATE 语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。
- 这个过程中,查询是被 block 住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
-
for update使用
- 如果遇到存在高并发并且对于数据的准确性很有要求的场景,需要使用for update来锁定数据。
- for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
- for update操作在未获取到数据的时候,mysql不进行锁 (no lock)。
- 获取到数据的时候,进行对约束字段进行判断,存在有索引的字段则进行 row lock 否则进行 table lock。
- 当使用 ‘<>’,'like’等关键字时,进行for update操作时,mysql进行的是table lock。
-- 测试进行 for update 锁定该条记录 set autocommit = 0; begin; select * from tab_order where id = 1 for update; -- 此时执行更新该条记录发现失败 update tab_order set product_id = 100 where id = 1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
2.XA模式
查看文章:分布式事务
3.TCC模式
-
基本简介
- TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作
- 事务发起方在一阶段执行 Try 方式
- 事务发起方在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
-
执行流程
- Try:资源的检测和预留
- Confirm:执行的业务操作提交,要求 Try 成功 Confirm 一定要能成功
- Cancel:预留资源释放
4.Saga模式
-
基本简介
- Saga 理论出自 Hector & Kenneth 1987发表的论文 Sagas。
- Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
- Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
- Saga 正向服务与补偿服务也需要业务开发者实现,因此是业务入侵的。
-
执行流程
- 分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。
- 如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
-
使用场景
- Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。
- 事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,可以使用 Saga 模式。
-
优缺点
-
优点
- 一阶段提交本地数据库事务,无锁,高性能
- 参与者可以采用事务驱动异步执行,高吞吐
- 补偿服务即正向服务的“反向”,易于理解,易于实现
-
缺点
- Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性
-
三、基于AT模式的环境搭建
我们模拟创建两个服务,一个订单服务,一个库存服务。当用户下订单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存。
1.SeataServer服务端
-
下载文件
https://github.com/seata/seata/releases
-
解压文件
-
修改配置
-
/conf/file.conf
service { #vgroup->rgroup # 修改事务组名称为:fsp_tx_group,和客户端自定义的名称对应 vgroup_mapping.fsp_tx_group = "default" # only support single node default.grouplist = "127.0.0.1:8091" # degrade current not support enableDegrade = false # disable disable = false # 单位:ms,s,m,h,d,默认是永久 max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } # 事务的日志存储 store { # 存储模式: file、db、redis mode = "file" ## 文件存储 file { dir = "sessionStore" maxBranchSessionSize = 16384 maxGlobalSessionSize = 512 fileWriteBufferCacheSize = 16384 sessionReloadReadSize = 100 flushDiskMode = async } ## 数据库存储 db { # 数据源druid、dbcp、hikari等等 datasource = "druid" dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "mysql" password = "mysql" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ## redis存储 redis { host = "127.0.0.1" port = "6379" password = "" database = "0" minConn = 1 maxConn = 10 maxTotal = 100 queryLimit = 100 } }
-
/conf/registry.conf
# 注册中心 registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 } zk { cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } # 配置中心 config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
-
/conf/logback.xml
<!--修改日志的存储位置--> <property name="LOG_HOME" value="F:\hliedu\cloud\seata-server-1.3.0\seata\logs"/>
-
-
启动服务
/bin/seata-server.bat
2.注册中心与配置中心
以Nacos为例
-
修改SeataServer的配置文件
# 注册中心 registry { type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "命名空间ID" cluster = "default" username = "nacos" password = "nacos" } } # 配置中心 config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "命名空间ID" group = "SEATA_GROUP" username = "nacos" password = "nacos" } }
-
添加配置文件到Nacos
-
创建空间
-
创建配置
通过 config.txt 和 nacos.config.sh 脚本把配置推送到Nacos上
# -h 主机、-p 端口号、-g 分组、-t 命名空间ID、-u 用户名、-w 密码 sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t ID -u nacos -w nacos
-
-
查看配置
3.相关数据库创建
创建两个数据库,一个用于存放订单信息,一个用于存放库存信息
-
订单数据库seata-order
-- 订单表 DROP TABLE IF EXISTS `order`; CREATE TABLE `order` ( `order_id` int(11) NOT NULL AUTO_INCREMENT, `item_id` int(11) DEFAULT NULL, `price` double(5,2) DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; -- 日志表 drop table `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;
-
库存数据库seata-stock
-- 库存表 DROP TABLE IF EXISTS `stock`; CREATE TABLE `stock` ( `item_id` int(11) NOT NULL AUTO_INCREMENT, `item_name` varchar(30) DEFAULT NULL, `item_num` int(3) DEFAULT NULL, PRIMARY KEY (`item_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- 日志表 drop table `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;
-
SeataServer必须创建的数据库
旧版本的Seata中有相关的Sql脚本
-- 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`) );
4.订单微服务搭建
-
添加依赖
<!--SpringCloud相关依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.0.RELEASE</version> </dependency> <!--数据库--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--Seata相关依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.1</version> </dependency>
-
添加配置
server: port: 8081 spring: application: name: order-service # Nacos配置 cloud: nacos: discovery: server-addr: localhost:8848 service: ${spring.application.name} # 数据源配置 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata-order?useSSL=false&useUnicode=true&characterEncoding=utf-8 username: root password: jhy # feign配置 feign: hystrix: enabled: true # Mybatis配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-aliases-package: com.jhy.orderservice.pojo # Seata配置(需要与Seata Server的配置完全一致) seata: enabled: true application-id: ${spring.application.name} tx-service-group: my_test_tx_group registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 namespace: 0d63643a-9958-49e6-88c1-5eb66a5e3b0d group: SEATA_GROUP username: "nacos" password: "nacos" config: type: file file: name: file.conf management: endpoints: web: exposure: include: '*'
-
启动器类
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(basePackages = "com.jhy.orderservice.client") @MapperScan("com.jhy.orderservice.dao") public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }
-
业务类
@Service public class OrderServiceImpl implements OrderService { private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); @Autowired private OrderDao orderDao; @Autowired private StockClient stockClient; @Override @GlobalTransactional public Boolean addOrder(Order order) { logger.info("----------------->下订单业务开始"); // 插入订单 orderDao.insert(order); // 减少库存 stockClient.updateStock(order.getItemId()); logger.info("----------------->下订单业务结束"); return true; } }
5.库存微服务搭建
-
添加依赖
上同
-
添加配置
上同
-
启动器类
上同
-
业务类
@Service public class StockServiceImpl implements StockService { private Logger logger = LoggerFactory.getLogger(StockServiceImpl.class); @Autowired private StockDao stockDao; @Override @Transactional public Boolean updateStock(Long itemId) { UpdateWrapper<Stock> wrapper = new UpdateWrapper<>(); wrapper.eq("item_id", itemId); wrapper.setSql("item_num=item_num-1"); logger.info("---------------->修改库存开始"); System.out.println(1/0); int update = stockDao.update(null, wrapper); logger.info("---------------->修改库存结束"); if(update > 0){ return true; } return false; } }
四、验证分布式事务
1.测试不使用分布式事务
-
查看初始数据表记录
-
订单表
-
库存表
-
-
业务端代码
-
订单业务
@Override @Transactional public Boolean addOrder(Order order) { }
-
库存业务
@Override @Transactional public Boolean updateStock(Long itemId) { // 添加异常 System.out.println(1/0); }
-
-
调用接口执行
-
查看执行后的数据表记录
发现库存表由于异常没有发生变化,但是订单表却增加了
-
订单表
-
库存表
-
2.测试使用分布式事务
-
查看初始数据表记录
-
订单表
-
库存表
-
-
业务端代码
-
订单业务
@Override @Transactional @GlobalTransactional public Boolean addOrder(Order order) { }
-
库存业务
@Override @Transactional public Boolean updateStock(Long itemId) { // 添加异常 System.out.println(1/0); }
-
-
调用接口执行
上同
-
查看执行后的数据表记录
发现库存表由于异常没有发生变化,订单表也没有增加,说明分布式事务起作用了
-
订单表
-
库存表
-
【源码地址】:GitHub