共计 7605 个字符,预计需要花费 20 分钟才能阅读完成。
@[toc]
网上对于分布式事务讲实践的多,讲实战的少,明天我想通过一个案例,来让小伙伴们感触一把分布式事务,咱们明天尽量少谈点实践。咱们明天的配角是 Seata!
分布式事务波及到很多实践,如 CAP,BASE 等,很多小伙伴刚看到这些实践就被劝退了,所以咱们明天不讲实践,咱们就看个 Demo,通过代码疾速体验一把什么是分布式事务。
1. 什么是 Seata?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简略易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 反对的事务模式有四种别离是:
- Seata AT 模式
- Seata TCC 模式
- Seata Saga 模式
- Seata XA 模式
Seata 中有三个外围概念:
- TC (Transaction Coordinator) – 事务协调者:保护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) – 事务管理器:定义全局事务的范畴,开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) – 资源管理器:治理分支事务处理的资源(Resource),与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为独自部署的 Server 服务端,TM 和 RM 为嵌入到利用中的 Client 客户端。
这些概念小伙伴们作为一个理解即可,不理解也能用 Seata,理解了更能了解 Seata 的工作原理。
2. 搭建 Seata 服务端
咱们先来把 Seata 服务端搭建起来。
Seata 下载地址:
- https://github.com/seata/seata/releases
目前最新版本是 1.4.2,咱们就应用最新版本来做。
这个工具在 Windows 或者 Linux 上部署差异不大,所以我这里就间接部署在 Windows 上了,不便一些。
咱们首先下载 1.4.2 版本的 zip 压缩包,下载之后解压,而后在 conf 目录中配置两个中央:
- 首先配置 file.conf 文件
file.conf 中配置 TC 的存储模式,TC 的存储模式有三种:
- file:适宜单机模式,全局事务会话信息在内存中读写,并长久化本地文件 root.data,性能较高。
- db:适宜集群模式,全局事务会话信息通过 db 共享,绝对性能差点。
- redis:适宜集群模式,全局事务会话信息通过 redis 共享,绝对性能好点,然而要留神,redis 模式在 Seata-Server 1.3 及以上版本反对,性能较高,不过存在事务信息失落的危险,所以须要开发者提前配置适宜以后场景的 redis 长久化配置。
这里咱们为了省事,配置为 file 模式,这样事务会话信息读写在内存中实现,长久化则写到本地 file,如下图:
如果配置 db 或者 redis 模式,大家记得填一下上面的相干信息。具体如下图:
题外话
留神,如果应用 db 模式,须要提前准备好数据库脚本,如下(小伙伴们能够间接在公众号江南一点雨后盾回复 seata-db 下载这个数据库脚本):
CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `seata2`;
/*Table structure for table `branch_table` */
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime(6) DEFAULT NULL,
`gmt_modified` datetime(6) DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `branch_table` */
/*Table structure for table `global_table` */
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `global_table` */
/*Table structure for table `lock_table` */
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (`row_key` varchar(128) NOT NULL,
`xid` varchar(128) DEFAULT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
另外还须要留神的是本人的数据库版本信息,改数据库连贯的时候依照理论状况批改,Seata 针对 MySQL5.x 和 MySQL8.x 都提供了对应的数据库驱动(在 lib 目录下),咱们只须要把驱动改好就行了。
- 再配置 registry.conf 文件
registry.conf 次要配置 Seata 的注册核心,咱们这里采纳大家比拟相熟的 Eureka,配置如下:
能够看到,反对的配置核心比拟多,咱们抉择 Eureka,选好配置核心之后,记得批改配置核心相干的信息。
OK,当初就配置实现了,然而先别启动,还差一个 Eureka 注册核心。
3. 我的项目配置
接下来咱们配置我的项目。
Seata 官网提供了一个十分经典的 Demo,咱们间接来看这个 Demo。
官网案例下载地址:https://github.com/seata/seata-samples
不过这里是很多案例混在一起的,可能看起来会比拟乱,而且因为要下载的依赖比拟多,所以极有可能依赖下载失败,因而大家也能够在公众号后盾回复 seata-demo
获取松哥整顿好的案例,间接导入即可,如下图:
这是一个商品下单的案例,我来和大家略微解释下:
- eureka:这是服务注册核心。
- account:这是账户服务,能够查问 / 批改用户的账户信息(次要是账户余额)。
- order:这是订单服务,能够下订单。
- storage:这是一个仓储服务,能够查问 / 批改商品的库存数量。
- bussiness:这是业务,用户下单操作将在这里实现。
这个案例讲了一个什么事呢?
当用户想要下单的时候,调用了 bussiness 中的接口,bussiness 中的接口又调用了它本人的 service,在 service 中,首先开启了全局分布式事务,而后通过 feign 调用 storage 中的接口去扣库存,而后再通过 feign 调用 order 中的接口去创立订单(order 在创立订单的时候,不仅会创立订单,还会扣除用户账户的余额),在扣除库存并实现订单创立之后,接下来会去检查用户的余额和库存数量是否正确,如果用户余额为正数或者库存数量为正数,则会进行事务回滚,否则提交事务。
本案例具体架构如下图:
这个案例就是一个典型的分布式事务问题,storage 和 order 中的事务分属于不同的微服务,然而咱们心愿他们同时胜利或者同时失败。
当初大家明确了这个案例是干嘛的,咱们就来把它跑起来。
首先创立一个名为 seata 的数据库,而后执行下面代码中的 all.sql 数据脚本。
接下来用 idea 关上下面这个我的项目,在每一个我的项目的 application.properties 文件中(Eureka 不必改),批改数据的连贯信息,如下图:
除了 Eureka 之外,另外四个都要改哦。
OK,配置完结。
4. 启动测试
首先启动 Eureka。
接下来先别记着启动其余服务,先启动 Seata Server,也就是咱们第二大节配置的那个服务,在它的 bin 目录下,Windows 下双击 /Linux 下执行启动脚本。
最初再别离启动剩下的四个服务,启动实现后,咱们能够在 Eureka 中查看相干信息:
能够看到,各个服务都注册上来了。
接下来咱们拜访 bussiness 中提供的两个测试接口。
第一个测试接口是:
http://127.0.0.1:8084/purchase/commit
这个接口对应的代码是:io.seata.sample.controller.BusinessController#purchaseCommit
,这个中央是模仿 U100000
用户购买了 30
个 C100000
商品,每个商品的价格是 100
,商品库存是 200
,用户账户余额是 10000
,所以购买之后,商品库存变为 170
,用户账户余额变为 7000
。这是失常购买的状况。
@RequestMapping(value = "/purchase/commit", produces = "application/json")
public String purchaseCommit() {
try {businessService.purchase("U100000", "C100000", 30);
} catch (Exception exx) {return exx.getMessage();
}
return "全局事务提交";
}
当咱们调完这个接口之后,就能够去数据库查看相应的数据。
第二个测试的接口是:
http://127.0.0.1:8084/purchase/rollback
这个接口对应的代码是:io.seata.sample.controller.BusinessController#purchaseRollback
,这次是模仿用户购买 99999
个商品,无论是用户账户余额还是商品库存数量,都无奈撑持这次购买行为,因而这个接口的调用最终会回滚,数据库中的数据会放弃原样。
@RequestMapping("/purchase/rollback")
public String purchaseRollback() {
try {businessService.purchase("U100000", "C100000", 99999);
} catch (Exception exx) {return exx.getMessage();
}
return "全局事务提交";
}
这就是一个分布式事务案例。
小伙伴们感兴趣也能够钻研一下官网这个案例,咱们会发现这里的货色非常简单,单纯是如下办法上多了一个注解而已(io.seata.sample.service.BusinessService#purchase
):
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {storageFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
if (!validData()) {throw new RuntimeException("账户或库存有余, 执行回滚");
}
}
purchase 办法用 @GlobalTransactional
注解标记了下,就开启了全局事务了,里边的两个调用都是 feign 的调用,对应了不同的服务,最初再做一个数据校验,校验失败就抛出异样,一旦该办法抛出异样,下面曾经执行的代码就会回滚。
这个我的项目其余的代码都是微服务中的惯例代码,就不赘述了。
5. 实现原理
咱们略微来说下 Seata 中这个分布式事务的原理,先来看一张图:
这张图十分清晰的形容了下面的案例,大抵流程如下:
- 有三个概念:TM、RM、TC,这些咱们在第一大节曾经介绍过了,这里就不再赘述。
- 首先由 Business 开启全局事务。
- 接下来 Business 在调用 Storage 和 Order 的时候,这两个在数据库操作之前都会向 TC 注册一个分支事务并提交。
- 分支事务在操作时,都会向 undo_log 表中提交一条记录,当全局事务提交的时候会清空 undo_log 表中的记录,否则将以该表中的记录为根据进行反向弥补(将数据恢复原样)。
具体到下面的案例,事务提交分两个阶段,过程如下:
一阶段:
- 首先 Business 开启全局事务,这个过程中会向 TC 注册,而后会拿到一个 xid,这是一个全局事务 id。
- 接下来在 Business 中调用 Storage 微服务。
- 来解析 SQL:失去 SQL 的类型(UPDATE),表(storage_tbl),条件(where commodity_code = ‘C100000’)等相干的信息。
- 查问前镜像:依据解析失去的条件信息,生成查问语句,定位数据。
- 执行业务 SQL,也就是做真正的数据更新操作。
- 查问后镜像:依据前镜像的后果,通过 主键 定位数据。
- 插入回滚日志:把前后镜像数据以及业务 SQL 相干的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
branch_id 和 xid 别离示意分支事务(即 Storage 本人的事务)和全局事务的 id,rollback_info 中保留着前后镜像的内容,这个将作为反向弥补(回滚)的根据,这个字段的值是一个 JSON,松哥挑出来这个 JSON 中比拟重要的一部分来和大家分享:
- beforeImage:这个是批改前数据库中的数据,能够看到每个字段的值,id 为 4,count 的值为 200。
- afterImage:这个是批改后数据库中的数据,能够看到,此时 id 为 4,count 的值为 170。
- Storage 在提交前,会向 TC 注册分支:申请 storage_tbl 表中,主键值等于 4 的记录的全局锁。
- 本地事务提交:业务数据的更新和后面步骤中生成的 UNDO LOG 一并提交。
- 同理,Order 和 Account 也依照下面的步骤提交数据。
以上 1-10 步就是一阶段的数据提交。
再来看二阶段:
二阶段有两种可能,提交或者回滚。
还是以下面的案例为例:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {storageFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
if (!validData()) {throw new RuntimeException("账户或库存有余, 执行回滚");
}
}
下单时候,扣除了库存,并且创立了订单,最初一查看,发现库存为正数或者用户账户余额为正数,阐明这个订单有问题,此时就该抛异样回滚,否则就提交数据。
具体操作如下:
回滚:
- 收到 TC 的分支回滚申请,开启一个本地事务,执行如下操作。
- 通过 xid 和 branch_id 去 undo_log 表中查找对应的记录。
- 数据校验:拿第二步查找到的后镜与以后数据进行比拟,如果有不同,阐明数据被以后全局事务之外的动作做了批改。这种状况,须要依据配置策略来做解决。
- 第三步的比拟如果雷同,则依据 undo_log 中的前镜像和业务 SQL 的相干信息生成并执行回滚的语句。
- 提交本地事务。并把本地事务的执行后果(即分支事务回滚的后果)上报给 TC。
提交:
- 收到 TC 的分支提交申请,把申请放入一个异步工作的队列中,马上返回提交胜利的后果给 TC。
- 异步工作阶段的分支提交申请将异步和批量地删除相应 UNDO LOG 记录。
换句话说,事务如果失常提交了,undo_log 表中是没有记录的,如果大家想看该表中的记录,能够在事务提交之前通过 DEBUG 的形式查看。
6. 小结
讲了这么多,是不是就把 Seata 讲完了呢?NONONO!这只是 AT 模式而已!还有三种模式,松哥下篇文章再和小伙伴们分享。
好啦,这就是一个简略的分布式事务,小伙伴们先来感触一把!题目是五分钟感触一把分布式事务,因为文章里边我还和大家分享了原理,如果大家只是跑一下案例感触,五分钟应该够了,不信试试!