十分钟带你体验一下什么是分布式事务

news2024/9/29 19:26:47

我们经常在网上看到很多人发关于分布式事务的理论,但是讲实战的却非常少,所以我今天想通过一个案例,来让小伙伴们都感受一下什么是分布式事务,这篇文章理论偏少,请文明观看。咱们今天的主角是 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 下载地址:

  • Releases · seata/seata · GitHub

目前最新版本是 1.4.2,我们就使用最新版本来做。

这个工具在 Windows 或者 Linux 上部署差别不大,所以我这里就直接部署在 Windows 上了,方便一些。

我们首先下载 1.4.2 版本的 zip 压缩包,下载之后解压,然后在 conf 目录中配置两个地方:

  1. 首先配置 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 目录下),我们只需要把驱动改好就行了。

  1. 再配置 registry.conf 文件

registry.conf 主要配置 Seata 的注册中心,我们这里采用大家比较熟悉的 Eureka,配置如下:

可以看到,支持的配置中心比较多,我们选择 Eureka,选好配置中心之后,记得修改配置中心相关的信息。

OK,现在就配置完成了,但是先别启动,还差一个 Eureka 注册中心。

3. 项目配置

接下来我们配置项目。

Seata 官方提供了一个非常经典的 Demo,我们直接来看这个 Demo。

官方案例下载地址:GitHub - seata/seata-samples: 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 用户购买了 30C100000 商品,每个商品的价格是 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 中这个分布式事务的原理,先来看一张图:

这张图非常清晰的描述了上面的案例,大致流程如下:

  1. 有三个概念:TM、RM、TC,这些我们在第一小节已经介绍过了,这里就不再赘述。
  2. 首先由 Business 开启全局事务。
  3. 接下来 Business 在调用 Storage 和 Order 的时候,这两个在数据库操作之前都会向 TC 注册一个分支事务并提交。
  4. 分支事务在操作时,都会向 undo_log 表中提交一条记录,当全局事务提交的时候会清空 undo_log 表中的记录,否则将以该表中的记录为依据进行反向补偿(将数据恢复原样)。

具体到上面的案例,事务提交分两个阶段,过程如下:

一阶段:

  1. 首先 Business 开启全局事务,这个过程中会向 TC 注册,然后会拿到一个 xid,这是一个全局事务 id。
  2. 接下来在 Business 中调用 Storage 微服务。
  3. 来解析 SQL:得到 SQL 的类型(UPDATE),表(storage_tbl),条件(where commodity_code = ‘C100000’)等相关的信息。
  4. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

  1. 执行业务 SQL,也就是做真正的数据更新操作。
  2. 查询后镜像:根据前镜像的结果,通过主键定位数据。

  1. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

branch_id 和 xid 分别表示分支事务(即 Storage 自己的事务)和全局事务的 id,rollback_info 中保存着前后镜像的内容,这个将作为反向补偿(回滚)的依据,这个字段的值是一个 JSON,松哥挑出来这个 JSON 中比较重要的一部分来和大家分享:

  • beforeImage:这个是修改前数据库中的数据,可以看到每个字段的值,id 为 4,count 的值为 200。
  • afterImage:这个是修改后数据库中的数据,可以看到,此时 id 为 4,count 的值为 170。


  1. Storage 在提交前,会向 TC 注册分支:申请 storage_tbl 表中,主键值等于 4 的记录的全局锁。
  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  3. 同理,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("账户或库存不足,执行回滚");
    }
}

下单时候,扣除了库存,并且创建了订单,最后一检查,发现库存为负数或者用户账户余额为负数,说明这个订单有问题,此时就该抛异常回滚,否则就提交数据。

具体操作如下:

回滚:

  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过 xid 和 branch_id 去 undo_log 表中查找对应的记录。
  3. 数据校验:拿第二步查找到的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
  4. 第三步的比较如果相同,则根据 undo_log 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。
  5. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

提交:

  1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

换句话说,事务如果正常提交了,undo_log 表中是没有记录的,如果大家想看该表中的记录,可以在事务提交之前通过 DEBUG 的方式查看。

6、小结

讲了这么多,是不是就把 Seata 讲完了呢?NONONO!这只是 AT 模式而已!还有三种模式,后面再和大家分享~

好啦,这就是一个简单的分布式事务,快来感受一下吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Django项目开发

一.认识NoSQL 1.SQL 关系型数据库 结构化: 定义主键,无符号型数据等关联的:结构化表和表之间的关系通过外键进行关联,节省存储空间SQL查询:语法固定 SELECT id,name,age FROM tb_user WHERE id1 ACID 2.NoSQL 非关系型数据库 Re…

通过实例告诉你lua中ipairs到底是怎么遍历的!

这个的文章挺多的,但是有好几种说法并且不全。有人说是忽略手动设定值,有人说是从1开始数,直到序号断开,还有人给出结果,但是和我实机测试的效果不一样, 所以我自己总结一篇。经过我的测试和总结得到以下结…

【2023】Prometheus-Alertmanager高可用集群

本次实验准备了三个节点,分别为laert-01~03 目录1.安装Alertmanager2.配置启动文件3.验证集群4.关于集群的配置项1.安装Alertmanager 这部分内容在三个节点上都要执行 下载安装包,将安装包解压至/data目录下 wget https://github.com/prometheus/aler…

javassm高校学生评教系统的设计与实现idea msyql

伴随着社会以及科学技术的发展,互联网已经渗透在人们的身边,网络慢慢的变成了人们的生活必不可少的一部分,紧接着网络飞速的发展,管理系统这一名词已不陌生,越来越多的学校、公司等机构都会定制一款属于自己个性化的管…

Scout:一款功能强大的轻量级URL模糊测试与爬取工具

关于Scout Scout是一款功能强大的轻量级URL模糊测试与爬取工具,可以帮助广大研究人员进行URL模糊测试,并爬取目标Web服务器中难以扫描发现的VHSOT、文件和目录等资源。 项目中包含了一个完整的字典文件,并尽可能地提供了更多的便携性&#…

【寻人启事】达坦科技持续招人ing

​​​​​​​ ❤️一起来探索前沿科技,做有意思的事情~ 我们是谁 达坦科技(DatenLord)专注于打造新一代开源跨云存储平台。通过软硬件深度融合的方式打通云云壁垒,实现无限制跨云存储、跨云联通,建立海量异地、异构…

活动星投票在时间的河流上造园分组怎么设置如何进行分组报名

“在时间的河流上造园”网络评选投票_免费小程序运行系统_企业有关的投票_微信投票的应用小程序投票活动如何做?很多企业在运营当中,都会通过投票活动来进行推广,从而达到吸粉、增加用户粘度等效果。而此类投票活动,通过小程序就可…

雅思经验(十一)

写作:WRITINGTASK 2Governments should spend money on railways rather than roads.To what extent do you agree or disagree with this statement?Give reasons for your answer and include any relevant examples from your own knowledge or experience.思路…

【性能优化】MySql查询性能优化必知必会

文章目录分析查询SQL查询优化器查询优化器的两种优化方式数据库存储结构数据库中的存储结构是怎样的数据页内的结构是怎样的索引索引是什么索引好坏的评价标准索引的数据结构B树B树B树是如何进行记录检索的?索引维护索引组织表二级索引索引设计覆盖索引函数索引前缀…

服务器如何下载百度网盘文件?Linux服务器如何在百度网盘中连接、上传下载;在Linux服务器上下载百度云盘中的资料

前言 百度云提供Python包bypy进行远程服务器的对接然后下载: https://github.com/houtianze/bypy 可以通过pip直接下载,授权本人的百度云账号后,就可以直接使Linux电脑本地文件与百度网盘的apps(我的应用数据)/bypy目…

【c语言】二叉树

主页:114514的代码大冒险 qq:2188956112(欢迎小伙伴呀hi✿(。◕ᴗ◕。)✿ ) Gitee:庄嘉豪 (zhuang-jiahaoxxx) - Gitee.com 引入 我们之前已经学过线性数据结构,今天我们将介绍非线性数据结构----树 树是一种非线性的…

Element UI框架学习篇(五)

Element UI框架学习篇(五) 1 准备工作 1.1 在zlz包下创建数据传输对象类EmpDTO package com.zlz.dto;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;//根据前台来的 Data public class EmpDTO {private String name;private Stri…

SpringBoot的配置与使用

SpringBoot简介 我们的Spring是包含了众多工具的IoC容器,而SpringBoot则是Spring的加强版,可以更加方便快捷的使用 如果Spring是手动挡的车,那么SpringBoot就是自动挡的车,让我们的驾驶体验变得更好 SpringBoot具有一下几种特征…

NSSCTF Round#8 Basic

from:http://v2ish1yan.top MyDoor 使用php伪协议读取index.php的代码 php://filter/readconvert.base64-encode/resourceindex.php<?php error_reporting(0);if (isset($_GET[N_S.S])) {eval($_GET[N_S.S]); }if(!isset($_GET[file])) {header(Location:/index.php?fi…

Excel中缺失数据值的自动填充

目录简单方法示例1&#xff1a;数据满足线性趋势示例2&#xff1a;数据满足增长(指数)趋势参考实验做完处理数据&#xff0c;发现有一组数据因为设备中途出现问题缺失了&#xff0c;之前做过的数据也找不到&#xff0c;为了不影响后续处理&#xff0c;这里使用Excel插入缺失值。…

一篇文章学习什么是进程(万字解析,超多知识点)

目录进程概念进程控制块-PCBPCB的内容分类标识符查看进程信息的方法状态fork函数进程状态R运行状态&#xff08;running&#xff09;S睡眠状态&#xff08;sleeping&#xff09;D磁盘休眠状态&#xff08;Disk sleep&#xff09;T停止状态&#xff08;stopped&#xff09;X死亡…

【数据结构】最小生成树(Prim算法,普里姆算法,普利姆)、最短路径(Dijkstra算法,迪杰斯特拉算法,单源最短路径)

文章目录前置问题问题解答一、基础概念&#xff1a;最小生成树的定义和性质&#xff08;1&#xff09;最小生成树&#xff08;Minimal Spanning Tree&#xff09;的定义&#xff08;2&#xff09;最小生成树&#xff08;MST&#xff09;的性质二、如何利用MST性质寻找最小生成树…

1.2(完结)C语言进阶易忘点速记

1.大端存储&#xff1a;高权位数字放在低地址处&#xff0c;低权位数字放在高地指处。(以字节为单位) 2.小端存储&#xff1a;低权位数字放在低地址处&#xff0c;高权位数字放在高地址处。(以字节为单位) 3.变量(char类型)进行运算的时候一定要注意整形提升与截断&#xff0…

五、Java框架之Maven进阶

黑马课程 文章目录1. 分模块开发1.1 分模块开发入门案例示例&#xff1a;抽取domain层示例&#xff1a;抽取dao层1.2 依赖管理2. 聚合和继承2.1 聚合概述聚合实现步骤2.2 继承 dependencyManagement3. 属性管理3.1 依赖版本属性管理3.2 配置文件属性管理&#xff08;了解&#…

Easy-Es框架实践测试整理 基于ElasticSearch的ORM框架

文章目录介绍&#xff08;1&#xff09;Elasticsearch java 客户端种类&#xff08;2&#xff09;优势和特性分析&#xff08;3&#xff09;性能、安全、拓展、社区&#xff08;2&#xff09;ES版本及SpringBoot版本说明索引处理&#xff08;一&#xff09;索引别名策略&#x…