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

news2024/11/14 17:47:32

涉及到的相关技术

     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:latest


LABEL 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:latest

LABEL maintainer="zyf021026 <shichuxin6@163.com>"

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


version: '3'

networks:
  backend:
    driver: bridge


######## 项目依赖的环境,启动项目之前要先启动此环境 #######
services:
  etcd:
    build:
      context: etcd
    environment:
      - TZ=Asia/Shanghai
      - ALLOW_NONE_AUTHENTICATION=yes
    ports: # 设置端口映射
      - "2379:2379"
    networks:
      - backend
    restart: always
  dtm:
    build:
      context: ./dtm
    environment:
      - TZ=Asia/Shanghai
    entrypoint:
      - "/app/dtm/dtm"
      - "-c=/app/dtm/configs/config.yaml"
    privileged: true
    volumes:
      - ./dtm/config.yml:/app/dtm/configs/config.yaml # 将 dtm 配置文件挂载到容器里
    ports:
      - "36789:36789"
      - "36790:36790"
    networks:
      - backend
    restart: always
    depends_on:
      - etcd

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

执行如下图中的命令:

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

/*
 Navicat Premium Data Transfer

 Source Server         : Link
 Source Server Type    : MySQL
 Source Server Version : 50743
 Source Host           : 39.101.77.206:3306
 Source Schema         : dtm_barrier

 Target Server Type    : MySQL
 Target Server Version : 50743
 File Encoding         : 65001

 Date: 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("用户标签回滚开始--->")
	// 获取 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 {
		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 bool
		err = 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--->开始")
	// 获取 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) {
		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 的BuildTarget
	userRpcBuildServer, err := l.svcCtx.Config.UserRpc.BuildTarget()
	if err != nil {
		return nil, status.Error(100, "用户注册异常")
	}
	// 获取TagRpc 的BuildTarget
	tagRpcBuildServer, 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)
	// 创建一个gid
	gid := dtmgrpc.MustGenGid(dtmServer)
	//创建一个自增id
	if _, 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 {
		//自增主键减少1
		if _, 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日志会记录更新前的数据到日志中,是在一个事务下执行过程中,在还没有提交之前,如果发生意外,就可以通过这个日志回滚到事务执行之前的数据了.

        

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

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

相关文章

绝地求生:【2024PGC之路——PUBG电竞积分分布】

亲爱的PUBG电竞爱好者&#xff0c; 你们好&#xff01; 2024年PUBG电竞即将开始&#xff0c;让我们一起深入了解下今年令人激动的PGS 和 PGC赛事积分分配情况。 PUBG GLOBAL SERIES&#xff08;PGS全球系列赛&#xff09;: 积分分布 根据我们之前概述的《PUBG 2024电竞计划》…

[项目设计] 从零实现的高并发内存池(一)

&#x1f308; 博客个人主页&#xff1a;Chris在Coding &#x1f3a5; 本文所属专栏&#xff1a;[高并发内存池] ❤️ 前置学习专栏&#xff1a;[Linux学习] ⏰ 我们仍在旅途 ​ 目录 前言 项目介绍 1.内存池 1.1 什么是内存池 池化技术 内存池 1.2 为什…

P9905 [COCI 2023/2024 #1] AN2DL 【矩阵区间最大值】

文章目录 题目大意1.输入格式2.输出格式3.数据范围与约定 思路维护每一行区间维护每一列区间维护区间最大值code↓ 完结撒花(&#xffe3;▽&#xffe3;) / 题目大意 给定 n , m , r , s n,m,r,s n,m,r,s 和一个 n m n\times m nm 的整数矩阵 A A A&#xff0c;求它每个 …

PyTorch-神经网络

神经网络&#xff0c;这也是深度学习的基石&#xff0c;所谓的深度学习&#xff0c;也可以理解为很深层的神经网络。说起这里&#xff0c;有一个小段子&#xff0c;神经网络曾经被打入了冷宫&#xff0c;因为SVM派的崛起&#xff0c;SVM不了解的同学可以去google一下&#xff0…

Android 多桌面图标启动, 爬坑点击打开不同页面

备注 &#xff1a; MainActivity 正常带界面的UI MainActivityBt 和 MainActivityUsb 是透明的&#xff0c;即 android:theme"style/TranslucentTheme" ###场景1:只有MainActivity 设置成&#xff1a;android:launchMode"singleTask" 点击顺序&#xff1…

外贸网站模板建站

测绘检测wordpress外贸主题 简洁实用的wordpress外贸主题&#xff0c;适合做测绘检测仪器设备的外贸公司使用。 https://www.jianzhanpress.com/?p5337 白马非马衣服WordPress外贸建站模板 白马非马服装行业wordpress外贸建站模板&#xff0c;适用于时间服装企业的官方网站…

ehcache3介绍和使用示例

介绍 EhCache是一个广泛使用的Java进程内缓存框架&#xff0c;具有快速和精干的特点。它提供了以下主要优势&#xff1a; 速度快&#xff1a;由于其直接在JVM进程中运行&#xff0c;EhCache的访问速度非常快&#xff0c;适合对响应时间要求较高的应用。 配置灵活&#xff1a;…

使用 Haproxy 搭建Web群集

Haproxy是目前比较流行的一种群集调度工具&#xff0c;同类群集调度工具有很多&#xff0c;如LVS 和Nginx。相比较而言&#xff0c;LVS.牲能最好&#xff0e;但是搭建相对复杂:Nginx的upstream模块支持群集功能&#xff0e;但是对群集节点健康检查功能不强&#xff0c;性能没有…

GEE:使用ReLu激活函数对单波段图像进行变换(以NDVI为例)

作者:CSDN @ _养乐多_ 本文将介绍在 Google Earth Engine (GEE)平台上,对任意单波段影像进行 ReLu 变换的代码。并以对 NDVI 影像像素值的变换为例。 文章目录 一、ReLu激活函数1.1 什么是 ReLu 激活函数1.2 用到遥感图像上有什么用?二、代码链接三、完整代码一、ReLu激活…

HTML5:七天学会基础动画网页6

CSS3自定义字体 ①&#xff1a;首先需要下载所需字体 ②&#xff1a;把下载字体文件放入 font文件夹里&#xff0c;建议font文件夹与 css 和 image文件夹平级 ③&#xff1a;引入字体&#xff0c;可直接在html文件里用font-face引入字体&#xff0c;分别是字体名字和路径 例…

【C++ 函数栈】栈区保存函数参数和函数调用的过程

目录 1 调用过程 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;C专栏&#x1f4a5; 标题&#xff1a;【C 函数栈】栈区保存函数参数和函数调用的过程❣️ 寄语&#xff1a;人生的意义或许可以发挥自己全部的潜力&#xff0c;所以加油吧&#xff…

循环队列与循环双端队列

文章目录 前言循环队列循环双端队列 前言 1、学习循环队列和循环双端队列能加深我们对队列的理解&#xff0c;提高我们的编程能力。 2、本文循环队列使用的是数组&#xff0c;循环双端队列用的是双向链表 3、题目连接&#xff1a;设计循环队列 &#xff0c;设计循环双端队列。 …

C++面试宝典第34题:整数反序

题目 给出一个不多于5位的整数, 进行反序处理。要求: 1、求出它是几位数。 2、分别输出每一位数字。仅数字间以空格间隔, 负号与数字之间不需要间隔。如果是负数,负号加在第一个数字之前, 与数字没有空格间隔。注意:最后一个数字后没有空格。 3、按逆序输出各位数字。逆序后…

华为od机试C卷-开源项目热度榜单

1、题目描述 某个开源社区希望将最近热度比较高的开源项目出一个榜单&#xff0c;推荐给社区里面的开发者。 对于每个开源项目&#xff0c;开发者可以进行关注(watch)、收藏(star)、fork、提issue、提交合并请求(MR)等。 数据库里面统计了每个开源项目关注、收藏、fork、issue…

适用于恢复iOS数据的 10 款免费 iPhone 恢复软件

现在&#xff0c;您可以获得的 iPhone 的存储容量比大多数人的笔记本电脑和台式电脑的存储容量还要大。虽然能够存储数千张高分辨率照片和视频文件、安装数百个应用程序并随身携带大量音乐库以供离线收听固然很棒&#xff0c;但在一个地方拥有如此多的数据可能会带来毁灭性的后…

LNOI省选祭录

写在前面 大概率爆零 你说得对&#xff0c;但是「辽宁省2024联合省选」是一款由「中国计算机学会」推出的一款「开放世界冒险游戏」&#xff0c;游戏发生在一个被称作「大连大学」的幻想世界&#xff0c;在这里&#xff0c;被神选中的人将被授予「xor魔法手杖」&#xff0c;引…

#WEB前端(浮动与定位)

1.实验&#xff1a; 2.IDE&#xff1a;VSCODE 3.记录&#xff1a; float、position 没有应用浮动前 应用左浮动和右浮动后 应用定位 4.代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><me…

matplotlib直方图

matplotlib直方图 假设你获取了250部电影的时长(列表a中), 希望统计出这些电影时长的分布状态(比如时长为100分钟到120分钟电影的数量, 出现的频率)等信息, 你应该如何呈现这些数据? from matplotlib import pyplot as plt a[131, 98, 125, 131, 124, 139, 131, 117, 128, …

linux逻辑卷管理

一.物理卷&#xff0c;逻辑卷&#xff0c;卷组的关系 二.实验题目 1.业务需要&#xff0c;新增5G硬盘&#xff0c;先对第一块磁盘分区&#xff0c;大小为4G&#xff0c;现在进行逻辑卷划分&#xff0c;卷组名为myvg,逻辑卷名为LV1&#xff0c;大小为2G 2.格式化逻辑卷LV1&#…

【论文阅读】多传感器SLAM数据集

一、M2DGR 该数据集主要针对的是地面机器人&#xff0c;文章正文提到&#xff0c;现在许多机器人在进行定位时&#xff0c;其视角以及移动速度与车或者无人机有着较大的差异&#xff0c;这一差异导致在地面机器人完成SLAM任务时并不能直接套用类似的数据集。针对这一问题该团队…