Seata1.5.2学习(二)——使用分布式事务锁@GlobalLock

目录

一、创建数据库 

二、配置consumer-service

1.pom.xml

2.application.properties

3.启动类

4.其他代码

三、配置provider-service

1.pom.xml

2.application.properties

3.启动类

4.其他代码

四、分布式事务问题演示与解决办法 

(一)分布式事务问题演示

(二)解决办法:使用@GlobalTransactional全局事务注解

五、SQL回滚日志——beforeImage和afterImage

六、@GlobalLock

(一)数据库脏写

(二)GlobalLock是如何预防数据库脏写的

七、面试重点:Seata分布式事务流程

1.一阶段 

2.二阶段-回滚

3.二阶段-提交


一、创建数据库 

每个库都创建下面的表:

-- 分布式事务回滚使用, sql在源码文件夹的 script\client\at\db 目录下
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

二、配置consumer-service

1.pom.xml

添加依赖

<!-- seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<!-- 连接MySQL数据库 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>

2.application.properties

添加下面的配置

#数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.157.102:3306/seata-user?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

# 用于在分布式中区分是哪个服务,应该保持唯一
seata.application-id=${spring.application.name}

# 获取配置
seata.config.type=nacos
seata.config.nacos.server-addr=192.168.157.102:8848
seata.config.nacos.namespace=seata
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.dataId=seata-server
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos

# 获取服务
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=192.168.157.102:8848
seata.registry.nacos.namespace=seata
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos

3.启动类

@SpringBootApplication
@EnableFeignClients // 开启openFeign注解扫描
@MapperScan("com.javatest.mapper")
// @EnableAutoDataSourceProxy是 Seata 提供的一个注解,用于启用数据源代理功能。
// 它的主要目的是为数据源创建一个代理对象,以便在分布式事务场景中进行拦截和增强。
@EnableAutoDataSourceProxy
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

4.其他代码

User实体类

@TableName("user")
public class User {
    @TableId
    private Integer id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

UserMapper

public interface UserMapper extends BaseMapper<User> {
}

UserService

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private ProviderService providerService;

    public void saveUser(User user) {
        userMapper.insert(user); // 操作第一个数据库

        // 发起远程调用,操作第二个数据库
        providerService.createUser(user);
    }
}

ProvicerService 中,新增:

@FeignClient(name = "provider-service")
public interface ProviderService {

    @GetMapping("/saveu") // /saveu这个接口名字必须与provider-service的controller中的一致
    String createUser(@SpringQueryMap User user);
}

新增 SeataController: 

@RestController
public class SeataController {

    @Autowired
    private UserServiceImpl userService;

    @GetMapping("/saveUser")
    public String save(User user) {
        userService.saveUser(user);
        return "success-----------";
    }
}

三、配置provider-service

1.pom.xml

添加依赖

<!-- seata -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<!-- 连接MySQL数据库 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>

2.application.properties

添加下面的配置,注意provider-service使用的是seata-user2数据库

#数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.157.102:3306/seata-user2?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

# 用于在分布式中区分是哪个服务,应该保持唯一
seata.application-id=${spring.application.name}

# 获取配置
seata.config.type=nacos
seata.config.nacos.server-addr=192.168.157.102:8848
seata.config.nacos.namespace=seata
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.dataId=seata-server
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos

# 获取服务
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=192.168.157.102:8848
seata.registry.nacos.namespace=seata
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.username=nacos
seata.registry.nacos.password=nacos

3.启动类

@SpringBootApplication
@EnableAutoDataSourceProxy
@MapperScan("com.javatest.mapper")
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class,args);
    }
}

4.其他代码

User实体类

@TableName("user")
public class User {
    @TableId
    private Integer id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

UserMapper 

public interface UserMapper extends BaseMapper<User> {
}

UserService

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    public void saveUser(User user) {
        userMapper.insert(user); // 操作第二个数据库
    }
}

在ProviderController中,新增 :

@Autowired
private UserServiceImpl userService;

@GetMapping("/saveu")
public String createUser(User user) {
    userService.saveUser(user);
    return "success-----------";
}

四、分布式事务问题演示与解决办法 

(一)分布式事务问题演示

按照上面配置,我们在Consumer-service的saveUser方法中制作一个错误:

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private ProviderService providerService;

    public void saveUser(User user) {
        userMapper.insert(user); // 操作第一个数据库

        // 发起远程调用,操作第二个数据库
        providerService.createUser(user);

        int i = 1 / 0;
    }
}

启动项目consumer-service和provider-service,浏览器输入:

http://localhost:8888/saveUser?id=1&name=张三

运行后控制台报错,2张表的数据都存入成功:


        当添加@Transactional后,第一个数据库不会写入,第二个数据库会成功写入,因为第一个数据库会因为代码未执行成功,被@Transactional拦截回滚,而第二个数据库没有添加@Transactional,所以不会被回滚,数据会被写入。

清空2个数据库,重启consumer-service服务,浏览器再次输入:

http://localhost:8888/saveUser?id=1&name=张三

结果: 

这种现象就是分布式事务问题,导致的数据不一致。

注意:

  • 从Spring Boot 1.0 开始,Spring Boot 默认启用了事务管理,因此通常不需要显式添加 @EnableTransactionManagement。
  • 只有在自定义事务管理器、非 Spring Boot 项目或禁用自动配置时,才需要显式添加 @EnableTransactionManagement。
  • 如果不确定事务管理是否启用,可以通过日志或测试方法验证。

(二)解决办法:使用@GlobalTransactional全局事务注解

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private ProviderService providerService;

//    @Transactional
    @GlobalTransactional // 开启全局事务
    public void saveUser(User user) {
        userMapper.insert(user); // 操作第一个数据库

        // 发起远程调用,操作第二个数据库
        providerService.createUser(user);

        int i = 1 / 0;
    }
}

        清理数据库,开启@GlobalTransactional,重启consumer-service,然后再次访问代码仍然报错,但是数据库中没有数据,证明分布式事务生效。

也可以在@GlobalTransactional后面添加下面的代码

@GlobalTransactional(rollbackFor = RuntimeException.class)

@GlobalTransactional不一定非要放在Service中,放在Controller上也可以。

五、SQL回滚日志——beforeImage和afterImage

        @GlobalTransactional的原理就是sql执行前后的beforeImage和afterImage,日志文件就在undo_log表中,但是因为已经回滚完毕,undo_log表中的日志数据会被删除,下面演示undo_log表中的日志数据:

将rollback_info存储的内容复制出来,到JSON在线解析格式化验证 - JSON.cn这个网站格式化:

如果你使用的是Navicat,右键这个字段内容,另存为json再格式化 

 

这段json日志就是分支回滚日志,最外层是大的事务,大事务里面有2个小的本地事务,每一个小的本地事务都称为分支事务。

beforeImage:sql执行之前,没有数据

afterImage:sql执行之后,有数据

Seata会根据这个日志进行提交和回滚。

六、@GlobalLock

(一)数据库脏写

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private ProviderService providerService;

    //    @Transactional
    // 开启全局事务
    @GlobalTransactional(rollbackFor = RuntimeException.class)
    public void saveUser(User user) {
//        userMapper.insert(user); // 操作第一个数据库
        userMapper.updateById(user);

        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
        }
        // 发起远程调用,操作第二个数据库
        providerService.createUser(user);

        int i = 1 / 0;
    }

    public void updateById(User user) {
        userMapper.updateById(user); // 更新第一个数据库
    }
}
@RestController
public class SeataController {

    @Autowired
    private UserServiceImpl userService;

    @GetMapping("/saveUser")
    public String save(User user) {
        userService.saveUser(user);
        return "success-----------";
    }

    @GetMapping("/updateUser")
    public String update(User user) {
        userService.updateById(user);
        return "success-----------";
    }
}

将seata-user数据库的user表添加数据,seata-user2数据库表中没有数据

重启consumer-service先访问/saveUser修改表中的值,数据修改成功

http://localhost:8888/saveUser?id=100&name=小乔

在20s之内,访问/updateUser,数据修改成功

http://localhost:8888/updateUser?id=100&name=周瑜

此时,后台会一直尝试回滚,并且一直提示回滚失败

将表中的数据手动改回小乔才能回滚成功,这种情况只能手动修改

 

回滚完成后,数据会回到最开始的张三:

(二)GlobalLock是如何预防数据库脏写的

GlobalLock:全局锁,必须配合 @Transactional 和 update 语句,才能使用

作用:提交数据前,先找 seata 获取全局锁,如果加锁失败就报异常,比如:

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private ProviderService providerService;
    
    // 开启全局事务
    @GlobalTransactional(rollbackFor = RuntimeException.class)
    public void saveUser(User user) {
//        userMapper.insert(user); // 操作第一个数据库
        userMapper.updateById(user);

        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
        }
        // 发起远程调用,操作第二个数据库
        providerService.createUser(user);

        int i = 1 / 0;
    }

    @GlobalLock
    @Transactional
    public void updateById(User user) {
        userMapper.updateById(user); // 更新第一个数据库
    }
}

浏览器分别访问: 

http://localhost:8888/saveUser?id=100&name=小乔http://localhost:8888/updateUser?id=100&name=周瑜http://localhost:8888/saveUser?id=100&name=小乔 

控制台报错:获取锁失败,脏数据未写入

这里获取锁报错是因为,saveUser方法上,使用了@GlobalTransactional 注解,该注解会自动在seata上添加一条全局锁信息,比如:

 

这些数据过一段时间会自动清除。

执行updateById方法时,因为有@GlobalLock注解,所以也会向seata发送请求,给id=100的数据加全局锁,但是全局锁已经存在了,所以该条数据不会被修改。

七、面试重点:Seata分布式事务流程

流程图:

Seata中有3个角色:

  • TC(Transaction Coordinator)——事务协调者:驱动全局事务提交或回滚,其实就是 Seata服务端。
  • TM(Transaction Manager)——事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务,其实就是Seata客户端。
  • RM(Resource Manager)——资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

TM 和 RM 都在应用程序中

seata 分布式事务分为两个阶段,以下面的 sql 为例:

update user set name = '张三' where name = '小乔';

1.一阶段 

  1. 通过GlobalTransactional注册全局事务,会得到一个XID(全局事务ID)
  2. 解析SQL:得到SQL的类型(UPDATE),表名(user),条件(name = '哈哈')等相关的信息(元数据)。
  3. 保存原镜像beforeImage:根据解析得到的条件信息,生成查询语句,定位数据,比如:
    select id, name from user where name = '哈哈';
  4. 执行业务SQL:更新这条记录的name为 '亚瑟'。
  5. 保存新镜像afterImage:根据前镜像的结果,通过主键定位数据,比如:
    select id, name, since from product where id = 1;
  6. 插入回滚日志:把新老镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到UNDO_LOG表中。
  7. 本地事务提交之前,向TC注册分支事务:申请 user 表中,主键值等于1的记录的全局锁
  8. 本地事务提交:业务数据的更新和前面步骤中生成的UNDO_LOG一并提交。
  9. 将本地事务提交的结果上报给TC
  10. 调用,执行另一个事务,并传递XID(放到 Request Header 中,TX_XID)

2.二阶段-回滚

  1. 收到TC的分支回滚请求,开启一个本地事务,执行如下操作:
  2. 通过XID和Branch ID(分支事务ID)查找到相应的UNDO_LOG记录。
  3. 数据校验:拿UNDO_LOG 中的afterImage与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。
  4. 根据UNDO_LOG中的beforeImage和业务SQL的相关信息生成并执行回滚的语句:
  5. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。

3.二阶段-提交

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024信息技术、信息安全、网络安全、数据安全等国家标准合集共125份。

2024信息技术、信息安全、网络安全、数据安全等国家标准合集&#xff0c;共125份。 一、2024信息技术标准&#xff08;54份&#xff09; GB_T 17966-2024 信息技术 微处理器系统 浮点运算.pdf GB_T 17969.8-2024 信息技术 对象标识符登记机构操作规程 第8部分&#xff1a;通用…

Linux基本指令(三)+ 权限

文章目录 基本指令grep打包和压缩zip/unzipLinux和windows压缩包互传tar&#xff08;重要&#xff09;Linux和Linux压缩包互传 bcuname -r常用的热键关机外壳程序 知识点打包和压缩 Linux中的权限用户权限 基本指令 grep 1. grep可以过滤文本行 done用于标记循环的结束&#x…

C语言番外篇(3)------------>break、continue

看到我的封面图的时候&#xff0c;部分读者可能认为这和编程有什么关系呢&#xff1f; 实际上这个三个人指的是本篇文章有三个部分组成。 在之前的博客中我们提及到了while循环和for循环&#xff0c;在这里面我们学习了它们的基本语法。今天我们要提及的是关于while循环和for…

开源嵌入式实时操作系统uC/OS-II介绍

一、uC/OS-II的诞生&#xff1a;从开源实验到行业标杆 背景与起源 uC/OS-II&#xff08;Micro-Controller Operating System Version II&#xff09;诞生于1992年&#xff0c;由嵌入式系统先驱Jean J. Labrosse开发。其前身uC/OS&#xff08;1991年&#xff09;最初作为教学工…

PH热榜 | 2025-02-23

1. NYX 标语&#xff1a;你智能化的营销助手&#xff0c;助你提升业绩。 介绍&#xff1a;NYX的人工智能助手简化了从头到尾的广告活动管理&#xff0c;帮助你轻松创建高转化率的广告&#xff0c;启动多渠道营销活动&#xff0c;并通过实时分析来优化表现。它还可以整合主要的…

设备唯一ID获取,支持安卓/iOS/鸿蒙Next(uni-device-id)UTS插件

设备唯一ID获取 支持安卓/iOS/鸿蒙(uni-device-id)UTS插件 介绍 获取设备唯一ID、设备唯一标识&#xff0c;支持安卓&#xff08;AndroidId/OAID/IMEI/MEID/MacAddress/Serial/UUID/设备基础信息&#xff09;,iOS&#xff08;Identifier/UUID&#xff09;&#xff0c;鸿蒙&am…

libwebsockets交叉编译全流程

libwebsocket中的webscoket加密功能需要依赖于Openssl库因此需要提前准备好openssl开源库。 交叉编译openssl 下面演示源码方式交叉编译OpenSSL为动态库。 创建个Websocket文件夹&#xff0c;把后续的成果物均放在这个文件中&#xff0c;文件夹中创建子文件夹OpenSSL和libWeb…

图片爬取案例

修改前的代码 但是总显示“失败” 原因是 修改之后的代码 import requests import os from urllib.parse import unquote# 原始URL url https://cn.bing.com/images/search?viewdetailV2&ccidTnImuvQ0&id5AE65CE4BE05EE7A79A73EEFA37578E87AE19421&thidOIP.TnI…

MAC快速本地部署Deepseek (win也可以)

MAC快速本地部署Deepseek (win也可以) 下载安装ollama 地址: https://ollama.com/ Ollama 是一个开源的大型语言模型&#xff08;LLM&#xff09;本地运行框架&#xff0c;旨在简化大模型的部署和管理流程&#xff0c;使开发者、研究人员及爱好者能够高效地在本地环境中实验和…

游戏引擎学习第119天

仓库:https://gitee.com/mrxiao_com/2d_game_3 上一集回顾和今天的议程 如果你们还记得昨天的进展&#xff0c;我们刚刚完成了优化工作&#xff0c;目标是让某个程序能够尽可能快速地运行。我觉得现在可以说它已经快速运行了。虽然可能还没有达到最快的速度&#xff0c;但我们…

deepseek清华大学第二版 如何获取 DeepSeek如何赋能职场应用 PDF文档 电子档(附下载)

deepseek清华大学第二版 DeepSeek如何赋能职场 pdf文件完整版下载 https://pan.baidu.com/s/1aQcNS8UleMldcoH0Jc6C6A?pwd1234 提取码: 1234 或 https://pan.quark.cn/s/3ee62050a2ac

树形DP(树形背包+换根DP)

树形DP 没有上司的舞会 家常便饭了&#xff0c;写了好几遍&#xff0c;没啥好说的&#xff0c;正常独立集问题。 int head[B]; int cnt; struct node {int v,nxt; }e[B<<1]; void modify(int u,int v) {e[cnt].nxthead[u];e[cnt].vv;head[u]cnt; } int a[B]; int f[B]…

基于 Python 的项目管理系统开发

基于 Python 的项目管理系统开发 一、引言 在当今快节奏的工作环境中&#xff0c;有效的项目管理对于项目的成功至关重要。借助信息技术手段开发项目管理系统&#xff0c;能够显著提升项目管理的效率和质量。Python 作为一种功能强大、易于学习且具有丰富库支持的编程语言&…

紫光同创开发板使用教程(二):sbit文件下载

sbit文件相当于zynq里面的bit文件&#xff0c;紫光的fpga工程编译完成后会自动生成sbit文件&#xff0c;因工程编译比较简单&#xff0c;这里不在讲解工程编译&#xff0c;所以我这里直接下载sbit文件。 1.工程编译完成后&#xff0c;可以看到Flow列表里面没有报错&#xff0c…

完美解决:.vmx 配置文件是由 VMware 产品创建,但该产品与此版 VMware Workstation 不兼容

参考文章&#xff1a;该产品与此版 VMware Workstation 不兼容&#xff0c;因此无法使用 问题描述 当尝试使用 VMware Workstation 打开别人的虚拟机时&#xff0c;可能会遇到以下报错&#xff1a; 此问题常见于以下场景&#xff1a; 从其他 VMware 版本&#xff08;如 ESX…

QQ登录测试用例报告

QQ登录测试用例思维导图 一、安全性测试用例 1. 加密传输与存储验证 测试场景&#xff1a;输入账号密码并提交登录请求。预期结果&#xff1a;账号密码通过加密传输&#xff08;如HTTPS&#xff09;与存储&#xff08;如哈希加盐&#xff09;&#xff0c;无明文暴露。 2. 二…

Docker内存芭蕾:优雅调整容器内存的极限艺术

title: “&#x1f4be; Docker内存芭蕾&#xff1a;优雅调整容器内存的极限艺术” author: “Cjs” date: “2025-2-23” emoji: “&#x1fa70;&#x1f4a5;&#x1f4ca;” 当你的容器变成内存吸血鬼时… &#x1f680; 完美内存编排示范 &#x1f4dc; 智能内存管家脚本…

计算机毕业设计SpringBoot+Vue.jst网上购物商城系统(源码+LW文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

IoT设备硬件攻击技术与接口漏洞利用

IoT设备硬件攻击技术 0x01 总线与接口概述 1.1 UART 概述 UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;是一种常见的串行通信协议&#xff0c;广泛用于嵌入式设备中进行调试和数据传输。它通过两根信号线&#xff08;TX和RX&#xff09;实现异…

Windows程序设计29:对话框之间的数据传递

文章目录 前言一、父子对话框之间的数据传递1.父窗口获取子窗口数据2.子窗口获取父窗口数据 二、类外函数调用窗口的操作1.全局变量方式2.参数传递方式 总结 前言 Windows程序设计29&#xff1a;对话框之间的数据传递。 在Windows程序设计28&#xff1a;MFC模态与非模态对话框…