Go-zero中分布式事务的实现(DTM分布式事务管理器,在一个APi中如何调用两个不同服务的rpc层,并保证两个不同服务之间的业务逻辑同时成功)

本文主要是介绍Go-zero中分布式事务的实现(DTM分布式事务管理器,在一个APi中如何调用两个不同服务的rpc层,并保证两个不同服务之间的业务逻辑同时成功),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

涉及到的相关技术

     1.DTM分布式事务管理器,解决跨数据库、跨服务、跨语言栈更新数据的一致性问题。

      2.SAGA事务模式,SAGA事务模式是DTM中常用的一种模式,简单易上手.(当然还有其它更多的事务模式,这里采用的SAGA只不过是其中一种较为简单的方法)

      3.Go-zero框架,ETCD服务注册...

更多内容移步至:go-zero 缩短从需求到上线的距离 和 介绍 | DTM开源项目文档

业务场景

        如果是在单体架构的业务当中,是不需要用到分布式事务的.单体架构中,涉及到需要保证多个事务同时成功的场景,只需要创建一个全局的事务对象 如:tx := db.Begin(),然后统一用这一个tx去管理接下来的业务逻辑即可.

        不清楚在一个api中如何调用其它服务rpc的可以看看我的另一篇博客中的一种解决办法:

go-zero标准的项目结构,以及如何使用docker-compose部署道linux服务器上-CSDN博客

        但是在go-zero框架的这种微服务中,比如说:我在一个用户服务的api中调用了用户服务rpc中注册的业务,并且同时还调用了标签服务的rpc层中的选择标签的业务. 那么,此时我就需要保证用户的注册和标签的选择这两个在不同服务下执行的业务逻辑同时成功.(总不能用户账号密码插入到的表中,但是突然断网了,导致标签没有选择上去吧,这个是不符合我的业务的).

DTM 环境搭建(Windows本地搭建)

        !!!!!!!!!!!!!! 这个环境请注意,是需要在你本地去搭建的,至于为什么,我会在后面解释,最重要的先把环境搭建起来吧! 我采用的是docker-compose去搭建.(如果不了解windows电脑如何配置docker环境,可以移步:)

Windows11电脑是如何搭建docker环境的-CSDN博客

        废话不多说,首先从搭建环境讲起.(我这里采用的是docker-compose搭建我需要的环境)

上图就是项目的结构

在dtm和etcd的目录下面各自新建一个Dockerfile文件,Dockerfile都不需要过多的配置,只需要用到最基础的镜像即可.在dtm的目录下还需要新建一个config.yml文件.

DTM下Dokcerfile以及config.yml的编写

FROM yedf/dtm:latestLABEL maintainer="zyf021026 <shichuxin6@163.com>"
# 指定要存储trans状态的存储驱动
# Store:### 默认存储驱动
#   Driver: 'boltdb'### redis 存储驱动
#   Driver: 'redis'
#   Host: 'localhost'
#   User: ''
#   Password: ''
#   Port: 6379### mysql 存储驱动
#   Driver: 'mysql'
#   Host: 'mysql'
#   User: 'root'
#   Password: '123456'
#   Port: 3306### postgres 存储驱动
#   Driver: 'postgres'
#   Host: 'localhost'
#   User: 'postgres'
#   Password: 'mysecretpassword'
#   Port: '5432'### 以下配置仅适用于 postgres/mysql 驱动
#   MaxOpenConns: 500
#   MaxIdleConns: 500
#   ConnMaxLifeTime: 5
#   TransGlobalTable: 'dtm.trans_global'
#   TransBranchOpTable: 'dtm.trans_branch_op'### 以下配置仅适用于 redis/boltdb 驱动
#   DataExpire: 604800 # Trans 过期时间
#   RedisPrefix: '{}'  # Redis 存储前缀MicroService:Driver: 'dtm-driver-gozero'           # 要处理注册/发现的驱动程序的名称Target: 'etcd://your-ip:2379/dtmservice' # 注册 dtm 服务的 etcd 地址EndPoint: 'your-ip:36790'# 以下配置的单位为'秒'
# TransCronInterval: 3
# TimeoutToFail: 35
# RetryInterval: 10# 日志等级
# LogLevel: 'info'

ETCD的Dockerfile文件编写

FROM bitnami/etcd:latestLABEL maintainer="zyf021026 <shichuxin6@163.com>"

使用docker-compose 构建镜像,启动容器


version: '3'networks:backend:driver: bridge######## 项目依赖的环境,启动项目之前要先启动此环境 #######
services:etcd:build:context: etcdenvironment:- TZ=Asia/Shanghai- ALLOW_NONE_AUTHENTICATION=yesports: # 设置端口映射- "2379:2379"networks:- backendrestart: alwaysdtm:build:context: ./dtmenvironment:- TZ=Asia/Shanghaientrypoint:- "/app/dtm/dtm"- "-c=/app/dtm/configs/config.yaml"privileged: truevolumes:- ./dtm/config.yml:/app/dtm/configs/config.yaml # 将 dtm 配置文件挂载到容器里ports:- "36789:36789"- "36790:36790"networks:- backendrestart: alwaysdepends_on:- etcd

   在根目录下面执行docker-compose up -d 将需要的环境搭建起来

执行如下图中的命令:

        新建子事务屏障的数据库(库名和表名请不要修改) 可以作为独立的一个数据库使用,没有必要把自己的项目数据库名称改为dtm_barrier

/*Navicat Premium Data TransferSource Server         : LinkSource Server Type    : MySQLSource Server Version : 50743Source Host           : 39.101.77.206:3306Source Schema         : dtm_barrierTarget Server Type    : MySQLTarget Server Version : 50743File Encoding         : 65001Date: 03/03/2024 13:57:48
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for barrier
-- ----------------------------
DROP TABLE IF EXISTS `barrier`;
CREATE TABLE `barrier`  (`id` bigint(22) NOT NULL AUTO_INCREMENT,`trans_type` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',`gid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',`branch_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',`op` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',`barrier_id` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',`reason` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'the branch type who insert this record',`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `gid`(`gid`, `branch_id`, `op`, `barrier_id`) USING BTREE,INDEX `create_time`(`create_time`) USING BTREE,INDEX `update_time`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1482 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

SAGA事务模式的使用

        简单说明一下,SAGA分布式事务模式,是没有办法携带返回值的,因此尽量此处要避免需要有返回值的业务场景.

        直接用代码来展示SAGA事务模式的使用方法吧!

    用户注册服务PRC的编写以及事务失败补偿机制的编写

这里不再演示proto文件是如何编写的

用户注册服务的rpc

func (l *UserCreateLogic) UserCreate(in *user.UserCreateRequest) (pd *user.UserCreateResponse, endErr error) {// 获取 RawDB// 注册db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()// 获取子事务屏障对象barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)if err != nil {return nil, status.Error(500, err.Error())}// 开启子事务屏障err = barrier.CallWithDB(db, func(tx *sql.Tx) error {// 加密密码pwd, _ := bcrypt.GetPwd(in.Password)// 插入用户数据_, err = tx.Exec("INSERT INTO users (id , created_at, updated_at, username, password, avatar, phone) VALUES (?,?, ?, ?, ?, ?, ?)", in.Id, time.Now(), time.Now(), in.Username, pwd, in.Avatar, in.Phone)//返回子事务执行失败if err != nil {return err}return nil})if err != nil {return nil, status.Error(codes.Aborted, dtmcli.ResultFailure) //如果失败,不再重试,直接回滚}return &user.UserCreateResponse{}, endErr
}

用户注册服务rpc的失败补偿 (如果注册服务的rpc失败,就会执行相应的补偿方法)

func (l *UserCreateRevertLoginLogic) UserCreateRevertLogin(in *user.UserCreateRequest) (pd *user.UserCreateResponse, err error) {fmt.Println("用户标签回滚开始--->")// 获取 RawDBdb, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()// 获取子事务屏障对象barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)if err != nil {return nil, status.Error(500, err.Error())}// 开启子事务屏障err = barrier.CallWithDB(db, func(tx *sql.Tx) error {fmt.Println("注册事务走入了补偿")//删除插入的标签数据 和 用户数据_, err = tx.Exec("DELETE FROM tb_user_tag where user_id = ?", in.Id)_, err = tx.Exec("DELETE FROM users where id = ?", in.Id)//返回子事务执行失败if err != nil {return err}return nil})if err != nil {fmt.Println("failed---->", err)return nil, err}fmt.Println("删除成功")fmt.Println("用户标签回滚结束--->")return &user.UserCreateResponse{}, nil
}

        标签服务Rpc的编写以及事务失败补偿机制的编写

标签服务的rpc

func (l *SignUserChooseTagLogic) SignUserChooseTag(in *tag.UserChooseTagRequest) (*tag.UserChooseTagRequest, error) {// 获取 RawDB// 注册账号时,选择标签db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()if err != nil {return nil, status.Error(500, err.Error())}// 获取子事务屏障对象barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)// 开启子事务屏障err = barrier.CallWithDB(db, func(tx *sql.Tx) (err error) {// 用户注册时选择标签var exists boolerr = tx.QueryRow("SELECT EXISTS(SELECT 1 FROM tb_user_tag WHERE tag_id = ? and user_id = ?)", in.TagId, in.UserId).Scan(&exists)if err != nil {return err}if exists {return fmt.Errorf("标签重复选择")}fmt.Println("开始插入标签")_, err = tx.Exec("INSERT INTO tb_user_tag (tb_user_tag.created_at , tb_user_tag.updated_at , tag_id, user_id) VALUES (?,?,?, ?)", time.Now(), time.Now(), in.TagId, in.UserId)if err != nil {return fmt.Errorf("标签选择失败")}return nil})if err != nil {return nil, status.Error(codes.Aborted, dtmcli.ResultFailure) //事务失败不再重试,直接回滚}return &tag.UserChooseTagRequest{}, nil
}

标签选择失败补偿的rpc

func (l *SignUserChooseTagRevertLogic) SignUserChooseTagRevert(in *tag.UserChooseTagRequest) (*tag.UserChooseTagRequest, error) {fmt.Println("用户标签SignUserChooseTagRevert--->开始")// 获取 RawDBdb, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()if err != nil {return nil, status.Error(500, err.Error())}// 获取子事务屏障对象barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)// 开启子事务屏障err = barrier.CallWithDB(db, func(tx *sql.Tx) (err error) {fmt.Println("注册时选择标签进入了补偿")logc.Info(l.ctx)//删除记录_, err = tx.Exec("DELETE FROM tb_user_tag where tag_id = ? and user_id = ?", in.TagId, in.UserId)return err})if err != nil {return nil, err}fmt.Println("用户标签SignUserChooseTagRevert--->结束")return &tag.UserChooseTagRequest{}, nil
}

至此rpc层的业务逻辑全部编写完毕,但请一定要注意每一个rpc的返回值,一定要按照 如&tag.UserChooseTagRequest{}返回,不能简单的返回一个nil值.否则会导致事务一直无法提交

        API层的编写


func (l *SignUpLogic) SignUp(req *types.UserCreateRequest) (resp *types.UserCreateResponse, err error) {//首先判断用户是否存在_, err = l.svcCtx.UserRpc.UserIsExists(l.ctx, &user.UserCreateRequest{Phone: req.Phone,})if err != nil {return nil, err}// 获取UserRpc 的BuildTargetuserRpcBuildServer, err := l.svcCtx.Config.UserRpc.BuildTarget()if err != nil {return nil, status.Error(100, "用户注册异常")}// 获取TagRpc 的BuildTargettagRpcBuildServer, err := l.svcCtx.Config.TagRpc.BuildTarget()if err != nil {return nil, status.Error(100, "标签选择异常")}empty := user.Empty{}//dtm服务的etcd注册地址var dtmServer = l.svcCtx.Config.Dtm//dtmServer := "etcd://etcd:2379/dtmservice"fmt.Println(dtmServer)// 创建一个gidgid := dtmgrpc.MustGenGid(dtmServer)//创建一个自增idif _, err := l.svcCtx.UserRpc.AddUserId(l.ctx, &empty); err != nil {return nil, fmt.Errorf("CREATE user id error:%v", err)}userID, _ := l.svcCtx.UserRpc.NextUserID(l.ctx, &empty)saga := dtmgrpc.NewSagaGrpc(dtmServer, gid).Add(tagRpcBuildServer+"/tag.TagSign/SignUserChooseTag", tagRpcBuildServer+"/tag.TagSign/SignUserChooseTagRevert", &tag.UserChooseTagRequest{UserId: userID.NextUserId,TagId:  req.StartTagId,}).Add(userRpcBuildServer+"/user.UserService/UserCreate", userRpcBuildServer+"/user.UserService/UserCreateRevertLogin", &user.UserCreateRequest{Username: req.Username,Password: req.Password,Avatar:   req.Avatar,Phone:    req.Phone,Id:       userID.NextUserId,})//事务提交if err := saga.Submit(); err != nil {//自增主键减少1if _, err := l.svcCtx.UserRpc.DecUserID(l.ctx, &empty); err != nil {logx.Error(err)}logx.Error(err)return nil, fmt.Errorf("saga submit error:%v", err)}return &types.UserCreateResponse{}, nil
}

上面代码的逻辑,相信如果各位接触到微服务,一定是可以理解的,由于saga的事务模式没有返回值,所以我通过redis生成一个自增id来使用,而不再采用mysql的自增主键id.

上面代码中的地址可以在rpc生成的pb.go中找到

自己的理解

        经历了长度一周多对分布式事务的研究,写一点自己的简单理解吧!(比较浅显)

        saga事务模式需要自己写事务的补偿方法,子事务屏障内的事务执行失败之后,就会执行对应的事务补偿方法!即回滚事务.补偿方法内写的便是对这一次执行的插入,修改语句的相反操作.比如我增加某一条数据,补偿内就写上对删除的操作.

        感觉和MySQL的Undo log 回滚日志很相似啊!Undo log日志会记录更新前的数据到日志中,是在一个事务下执行过程中,在还没有提交之前,如果发生意外,就可以通过这个日志回滚到事务执行之前的数据了.

        

这篇关于Go-zero中分布式事务的实现(DTM分布式事务管理器,在一个APi中如何调用两个不同服务的rpc层,并保证两个不同服务之间的业务逻辑同时成功)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/772270

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

2. c#从不同cs的文件调用函数

1.文件目录如下: 2. Program.cs文件的主函数如下 using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using System.Windows.Forms;namespace datasAnalysis{internal static

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

uva 10061 How many zero's and how many digits ?(不同进制阶乘末尾几个0)+poj 1401

题意是求在base进制下的 n!的结果有几位数,末尾有几个0。 想起刚开始的时候做的一道10进制下的n阶乘末尾有几个零,以及之前有做过的一道n阶乘的位数。 当时都是在10进制下的。 10进制下的做法是: 1. n阶位数:直接 lg(n!)就是得数的位数。 2. n阶末尾0的个数:由于2 * 5 将会在得数中以0的形式存在,所以计算2或者计算5,由于因子中出现5必然出现2,所以直接一