【分布式事务】Seata AT实战

news2024/12/24 9:06:19

目录

Seata 介绍

Seata 术语

Seata AT 模式

介绍

实战(nacos注册中心,db存储)

部署 Seata

实现 RM

实现 TM

可能遇到的问题

1. Seata 部署成功,服务启动成功,全局事务不生效 

2. 服务启动报错 can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry

debug 调试

undo_log

branch_table

global_table

lock_table

原理

RM & TM 如何与 TC 建立连接

一阶段步骤

二阶段步骤

AT 模式如何避免脏写和脏读

总结


项目地址:GitHub - chenyukang1/mall

本文实战基于如下版本:
JDK 8
Spring Boot  2.6.11
Spring Cloud 2021.0.4
Spring Cloud Alibaba 2021.0.4.0
Seata 1.5.2

Seata 介绍

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案

  • 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入

  • 高性能:减少分布式事务解决方案所带来的性能消耗

源码:https://github.com/seata/seata(opens new window)

文档:Apache Seata

Seata 术语

TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚

TM (Transaction Manager) 事务管理器:开始全局事务、提交或回滚全局事务

RM (Resource Manager) 资源管理器:管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

image-20210215174627345

Seata AT 模式

介绍

AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等

在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作

实战(nacos注册中心,db存储)

部署 Seata

完成 db 建表,nacos 发布 seataServer.properties 配置,最后启动 seata,参考:Docker compose部署 | Apache Seata

实现 RM

1. 创建订单和库存服务的 DB 和表

-- 库存服务DB执行
CREATE TABLE `tab_storage` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
  `total` int(11) DEFAULT NULL COMMENT '总库存',
  `used` int(11) DEFAULT NULL COMMENT '已用库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO `tab_storage` (`product_id`, `total`,`used`)VALUES ('1', '96', '4');
INSERT INTO `tab_storage` (`product_id`, `total`,`used`)VALUES ('2', '100','0');
-- 订单服务DB执行
CREATE TABLE `tab_order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
  `count` int(11) DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完成',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2. 各数据库加入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;

3. 编写业务代码

实现创建订单 & 锁库存逻辑 

    public boolean save(long userId, long productId) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setUserId(userId);
        orderEntity.setProductId(productId);
        orderEntity.setCount(1);
        orderEntity.setMoney(BigDecimal.valueOf(80));
        orderEntity.setStatus(0);

        return save(orderEntity);
    }
    public boolean lockStock(long productId, long used) {
        return storageDao.lockStock(productId, used) > 0;
    }
    <update id="lockStock">
        UPDATE tab_storage
        SET used = used + #{num}
        WHERE
            product_id = #{productId}
          AND total - used >= #{num}
    </update>
实现 TM

TM 作为事务全局管理者,也是全局事务的发起者,通过远程调用订单和库存服务,开启全局事务

1. 编写 Feign 远程调用

2. 开启全局事务

    @GlobalTransactional(timeoutMills = 300000)
    @GetMapping("/submitOrder")
    public R submitOrder(long userId, long productId, long used) {
        businessService.submitOrder(userId, productId, used);

        return R.ok().put("res", "success");
    }
    @Override
    public void submitOrder(long userId, long productId, long used) {
        log.info("submitOrder begin ... xid: {}", RootContext.getXID());
        R lockStock = storageFeignService.lockStock(productId, used);
        boolean lockRes = (boolean) lockStock.get("res");
        if (!lockRes) {
            throw new RuntimeException("lock stock fail");
        }
        R save = orderFeignService.save(userId, productId);
        boolean saveRes = (boolean) save.get("res");
        if (!saveRes) {
            throw new RuntimeException("save order fail");
        }
    }
可能遇到的问题
1. Seata 部署成功,服务启动成功,全局事务不生效 

首先检查是否有全局事务 ID xid,请求的时候会通过请求头传递到下游服务,没有这个一切白搭。可以直接在全局事务的入口打印出来看看,代码示例:

log.info("submitOrder begin ... xid: {}", RootContext.getXID()); 

如果全局事务 ID 为 null,可能的原因有:

  • 版本问题:如果选用较低版本的 Seata(比如v1.5.2),适当降低 Spring Boot、Spring Cloud、Spring Cloud Alibaba 的配套版本,实在不确定可以参考文章开头我的版本配置
2. 服务启动报错 can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry

这个报错是因为 TM/RM 的 service.vgroupMapping.xx 配置与 Seata Server 的不一致,可以按如下方式排查:

    1. TM/RM 配置指定了事务群组

     

    2. 服务端有对应的配置(以 nacos 为例)

    

    3. TM/RM 的 nacos 注册中心必须和 Seata 在同一 namespace、同一 group(默认是 SEATA_GROUP) 下
    4. Seata 使用 nacos 部署,它读的配置默认是 seataServer.properties,而 TM/RM 的配置要通过官方提供的脚本发布到 nacos 与 Seata 同一命名空间下 ,推荐阅读:Nacos 配置中心 | Apache Seata

debug 调试

我们在全局事务开启后,结束前打断点,看看数据库发生了什么

undo_log

发现 RM 的 undo_log 表中都生成了一条记录,以库存表为例,字段的数据如下

{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"116.198.200.0:8091:45570721124696075","branchId":45570721124696077,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"tab_storage","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"tab_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":-5,"value":["java.lang.Long",2]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"used","keyType":"NULL","type":4,"value":44}]]}]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"tab_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"id","keyType":"PRIMARY_KEY","type":-5,"value":["java.lang.Long",2]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"used","keyType":"NULL","type":4,"value":46}]]}]]}}]]}
  • xid:全局事务 id
  • branchId:分支事务 id
  • beforeImage:事务前快照
  • afterImage:事务后快照
branch_table

TC 的 branch_table 新增两条记录,表示开启两个分支事务

global_table

global_table 新增一条记录,表示开启一个全局事务

lock_table

lock_table 新增两条记录,表示两个 RM 一阶段开启了事务,但事务未提交,都持有行锁

原理

RM & TM 如何与 TC 建立连接

在启动阶段,RM/TM 会在控制台打出注册信息,即与 TC 建立了连接

NettyPool create channel to transactionRole:TMROLE,address:116.198.200.0:8091,msg:< RegisterTMRequest{applicationId='seata-server', transactionServiceGroup='business-tx-service-group'} >

不难看出,它们之间的通信基于 Netty,Netty 作为一款高性能的 RPC 通信框架,保证了 TC 与 RM 之间的高效通信

但它又是怎么区分 RM 还是 TM 的?毕竟配置文件都一样。答案是 @GlobalTransactional 注解,这个注解表示开启全局事务,Seata 认为标注这个注解的客户端就是 TM,这类注解都是基于 Spring AOP 机制,对使用了注解的 Bean 方法分配对应的拦截器进行增强,来完成对应的处理逻辑。而 GlobalTransactionScanner 这个 Spring Bean,就承载着为各个注解分配对应的拦截器的职责

推荐阅读:Seata应用侧启动过程剖析——RM & TM如何与TC建立连接 | Apache Seata

一阶段步骤
  1. RM 写表的过程,Seata 会拦截业务 SQL,首先解析 SQL 语义
  2. 在业务数据被更新前,做一次快照,生成 beforeImage
  3. 执行业务 SQL
  4. 在业务数据更新之后,做一次快照,生成 afterImage,最后生成行锁

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

二阶段步骤

因为业务 SQL 在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可

  • 正常:TM 执行成功,通知 TC 全局提交,TC 此时通知所有的 RM 提交成功,删除 undo_log  回滚日志
  • 异常:TM 执行失败,通知 TC 全局回滚,TC 此时通知所有的 RM 进行回滚,根据 undo_log 反向操作,使用 beforeImage 还原业务数据,删除 undo_log。但在还原前要首先要校验脏写,对比 “数据库当前业务数据” 和 “afterImage”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写会按配置的策略处理
AT 模式如何避免脏写和脏读

推荐阅读:Seata AT 模式 | Apache Seata

总结

优点:

  • 无侵入性:本质上是通过代理数据源实现 2PC 模式,对业务无侵入性,开发成本低

缺点:

  • 不适合高并发场景:AT 模式的实现依赖数据库锁机制,本地事务依赖行锁来实现读写隔离,以电商中常见的提交订单业务为例,提交订单的业务流程涉及到创建订单,锁库存等等,订单是用户维度的数据,并发度不高;但库存记录是 sku 级别的,加行锁很容易让后续的读写请求都阻塞

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

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

相关文章

Windows安装jdk配置环境变量(基础)

一、下载安装JDK 下载地址:https://www.oracle.com/java/technologies/downloads/?er=221886#java8-windows 因为JDK8比较稳定,所以建议选择这个。电脑32位的下载jdk-8u411-windows-i586.exe;电脑是64位的下载jdk-8u411-windows-x64.exe 1、根据自己电脑的配置下载相应的…

C++使用Poco库封装一个FTP客户端类

0x00 Poco库中 Poco::Net::FTPClientSession Poco库中FTP客户端类是 Poco::Net::FTPClientSession , 该类的接口比较简单。 上传文件接口&#xff1a; beginUpload() , endUpload() 下载文件接口&#xff1a; beginDownload() , endDownload() 0x01 FTPCli类说明 FTPCli类…

Docker(六)-本地镜像发布到私有库

1.下载镜像Docker Registry 用于搭建私人版本Docker Hub docker pull registry2.运行私有库Registry 运行私有库Registry&#xff0c;相当于本地有个私有Docker hubdocker run -d -p hostPort:containerPort -v 【宿主机目录】:【容器目录】 --privilegedtrue 【私有库镜像】…

群晖NAS部署VoceChat私人聊天系统并一键发布公网分享好友访问

文章目录 前言1. 拉取Vocechat2. 运行Vocechat3. 本地局域网访问4. 群晖安装Cpolar5. 配置公网地址6. 公网访问小结 7. 固定公网地址 前言 本文主要介绍如何在本地群晖NAS搭建一个自己的聊天服务Vocechat&#xff0c;并结合内网穿透工具实现使用任意浏览器远程访问进行智能聊天…

PS添加物体阴影

一、选择背景&#xff0c;确保物体和北京分割出图层 二、右键单击物体图层&#xff0c;点击混合选项&#xff0c;点击投影 三、调整参数&#xff0c;可以看效果决定(距离是高度&#xff0c;扩展是浓度&#xff0c;大小是模糊程度)&#xff0c;保存即可

dp经典问题:LCS问题

dp&#xff1a;LCS问题 最长公共子序列&#xff08;Longest Common Subsequence, LCS&#xff09;问题 是寻找两个字符串中最长的子序列&#xff0c;使得这个子序列在两个字符串中出现的相对顺序保持一致&#xff0c;但不要求连续。 力扣原题链接 1.定义 给定两个字符串 S1…

Apple - Game Center Programming Guide

本文翻译整理自&#xff1a;Game Center Programming Guide&#xff08; Updated: 2016-06-13 https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/GameKit_Guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008304 文章…

什么是zip格式?zip格式文件要怎么打开,一文详解

Zip是一种常见的压缩文件格式&#xff0c;广泛应用于文件和文件夹的打包和压缩。它的使用方便、文件体积小&#xff0c;是网络传输和存储文件时的常用选择。本文将深入介绍Zip格式的定义、特点以及它在现代计算机应用中的重要性。 zip是什么文件&#xff1f; ZIP是一种相当简单…

专业竞赛组织平台赛氪网,引领大学生竞赛新时代

随着互联网技术的快速发展&#xff0c;高校学科竞赛组织和管理正迎来新的变革。环球赛乐&#xff08;北京&#xff09;科技有限公司&#xff08;以下简称”赛氪网“&#xff09;&#xff0c;作为一家专业竞赛组织平台不仅致力于大学生成长和前途的拓展&#xff0c;更在推动学科…

【Android】Android Studio 使用Kotlin写代码时代码提示残缺问题解决

问题描述 Android Studio升级之后&#xff0c;从Android Studio 4.2升级到Android Studio Arctic Fox版本&#xff0c;因为项目比较老&#xff0c;使用的Gradle 版本是3.1.3&#xff0c;这个版本的Android Studio最低支持Gradle 3.1版本&#xff0c;应该算是比较合适的版本。 …

【Redis】如何保证缓存和数据库的一致性

目录 背景问题思路 三个经典的缓存模式Cache-Aside读缓存写缓存为什么是删除旧缓存而不是更新旧缓存&#xff1f;为什么不先删除旧的缓存&#xff0c;然后再更新数据库&#xff1f; 延迟双删如何确保原子性 Read-Through/Write-ThroughRead-ThroughWrite-Through Write Behind …

LINUX桌面运维----第一天

一、Linux的特点&#xff1a; &#xff08;1&#xff09;与UNIX兼容 &#xff08;2&#xff09;自由软件&#xff0c;源码公开 &#xff08;3&#xff09;性能高&#xff0c;安全性强 &#xff08;4&#xff09;便于定制和再开发 &#xff08;5&#xff09;相互之间操作性…

某同盾验证码

⚠️前言⚠️ 本文仅用于学术交流。 学习探讨逆向知识&#xff0c;欢迎私信共享学习心得。 如有侵权&#xff0c;联系博主删除。 请勿商用&#xff0c;否则后果自负。 网址 aHR0cHM6Ly9zZWMueGlhb2R1bi5jb20vb25saW5lRXhwZXJpZW5jZS9zbGlkaW5nUHV6emxl 1. 先整体分析一下接…

地级市绿色创新及碳排放与环境规划数据(2000-2021年)

数据简介&#xff1a;分享各个城市对于碳排放的降低做出了哪些共享。该数据是地级市2000-2021年间由绿色创新、碳排放与环境规制数据构成的能源与环境研究数据大合集&#xff0c;并对其进行可视化处理&#xff0c;供大家研究使用。当今我国大力推进生态文明建设、美丽中国建设等…

【Python系列】FastAPI 中的路径参数和非路径参数解析问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

PDF秒变翻页式电子画册

​在当今数字化时代&#xff0c;将PDF文档转换成翻页式电子画册是一种提升作品展示效果和传播效率的有效方式。以下是将PDF秒变翻页式电子画册的攻略&#xff0c;帮助您轻松掌握数字创作技巧。 首先&#xff0c;选择一个合适的制作工具是关键。目前市场上有多种在线平台和软件可…

元素旋转?一个vue指令搞定

说在前面 &#x1f388;鼠标控制元素旋转功能大家应该都不陌生了吧&#xff0c;今天我们一起来看看怎么编写一个vue指令来实现元素旋转功能吧&#xff01; 效果展示 体验地址 http://jyeontu.xyz/jvuewheel/#/JRotateView 实现思路 1、自定义指令对象 export default {inse…

选择门店收银系统要考虑哪些方面?美业系统Java源码分享私

开店前的一个重要事件就是选择门店收银软件/系统&#xff0c;尤其是针对美容、医美等美业门店&#xff0c;一个优秀专业的系统十分重要&#xff0c;它必须贴合门店的经营需求&#xff0c;提供更全面、便捷、高效的管理功能&#xff0c;帮助提升门店的服务质量和经营效益。 以下…

品牌策划背后的秘密:我为何对此工作情有独钟?

你是否曾有过一个梦想&#xff0c;一份热爱&#xff0c;让你毫不犹豫地投身于一个行业&#xff1f; 我就是这样一个对品牌策划充满热情的人。 从选择职业到现在&#xff0c;我一直在广告行业里“混迹”&#xff0c;一路走来&#xff0c;也见证了许多对品牌策划一知半解的求职…

CPsyCoun:心理咨询多轮对话自动构建及评估方法

CPsyCoun: A Report-based Multi-turn Dialogue Reconstruction and Evaluation Framework for Chinese Psychological Counseling 在大模型应用于心理咨询领域&#xff0c;目前开源的项目有&#xff1a; https://github.com/SmartFlowAI/EmoLLM &#xff08;集合&#xff0c;…