说明:在分布式架构下,一个请求需要多个微服务来实现。当一个请求牵扯到多个微服务时,事务问题就变得麻烦起来。
问题描述
现在有三个服务,分别是账户服务、库存服务和订单服务,生成一个订单,需要确保商品的库存是否充足,账户的余额是否充足,实际操作的是三条语句(添加订单、修改账户余额、修改库存数量),这三条语句组成一个事务。
例如,2.3 库存不充足,修改数据库库存数量时报错,此时对账户表的操作,对订单表的新增订单操作均已完成,正常业务逻辑是需要将此次业务相关的操作全部回退。
业务代码
Step1:数据库相关表初始状态
Step2:创建订单代码
Step3:发送请求
请求数量大于库存数量
Step4:请求出错
库存为正整数,修改后结果小于0,修改出错;
Step5:数据库状态变化
金额已扣除,事务未完全回滚
解决方案
可以使用Seata技术解决,Seata是蚂蚁金服和阿里巴巴共同开源的,致力于提供高性能和简单医用的分布式事务服务,解决分布式事务问题。
Seata解决分布式事务问题,简单来说,是建立了一个TC(事务协调者,Transaction Coordinator),用来维护全局和分支事务的状态,协调全局事务提交/回滚,如下图:
图中的TM(事务管理器,Transaction Manager)可以理解为@GlobalTransactional注解标志的范围,如上面的新建订单方法;RM(Resource Manager,资源管理器)表示分支事务所管理的资源,可以简单理解为分支事务所控制的数据库表;
而Seata分布式事务具体实现分为有四种模式,特点如下:
本文详细介绍前两种模式的使用,其他请参考:http://t.csdn.cn/XZotl
代码实现
第一步:添加依赖
使用Seata前,先添加依赖,因为每个微服务都需要添加,我这里添加到父模块中。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
第二步:设置配置文件
设置seata的配置文件,注意每一个服务模块都需要添加
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
# 参考tc服务自己的registry.conf中的配置
type: nacos
nacos: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server # tc服务在nacos中的服务名称
cluster: SH
tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: SH
添加配置时,可敲一下“seata”看idea有没有提示,验证依赖是否添加进来了;
第三步:设置seata配置文件
在nacos上设置seata的配置文件,详细参考:http://t.csdn.cn/MOuRz;
第四步:创建数据库
为seata创建一个专门的数据库,用来记录事务信息,数据库名要与nacos上配置的数据库名一致
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
# 分支事务信息
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
# 全局事务信息
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
# 全局锁信息
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
第五步:创建快照表
创建一个表(updo_log),用来记录数据快照的信息,该表创建在业务的数据库内;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
AT 模式(默认)
原理图
AT模式的特点是,先执行SQL,当分支事务有失败时,再根据数据快照回滚数据。
第一步:添加配置
在各个服务模块里添加分布式事务的模式为AT:
seata:
data-source-proxy-mode: AT
第二步:修改注释
在需要添加事务的方法上,将“@Transactional”注解修改注解为“@GlobalTransactional”
第三步:启动测试
断点打在判断库存数量是否大于请求数量这一行代码,查看数据库各表的状态
各个表的状态
放开断点,查看各张表的状态,可以看到,因为库存判断执行失败,请求数量大于库存数量,事务回滚,各张表回滚到初始状态,分布式事务问题得到解决。
小结
因为AT模式是先执行SQL语句,所以当多线程并发时,如果先执行了SQL语句,此时其他线程来操作数据库,就出现了数据脏读。使用AT模式,需要解决数据脏读问题。
Seata的解决方法是,引入全局锁,即对全局事务建立一个锁,分支事务操作完数据库,此时也不允许其他线程来操作数据,只有全局事务执行完毕后,才释放锁。
(断点时,加全局锁)
(放过断点,释放锁)
XA 模式
原理图
XA模式比AT模式结构清晰的多,就很简单,由事务协调者统一协调执行,一旦发现有分支事务执行失败,就回滚所有分支事务,如下:
(正常情况)
(异常情况)
第一步:修改配置
修改各模块的配置,模式改为XA,重启所有服务;
seata:
registry:
data-source-proxy-mode: XA
第二步:启动测试
同样,断点打在判断库存这一行代码,查看各张表的内容;
另外几张表情况,可以发现表的内容情况与XA实现描述是符合的;
断点拿掉,程序报错;
数据未发现变化,XA模式同样解决了分布式事务问题;
小结
XA模式需锁定数据库资源,等待二阶段结束才释放,性能较差,但实现简单,不需要考虑数据脏读的问题。
总结
Seata可解决分布式事务问题,并提供了四种模式,AT为默认模式;