Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >面试高频:MySQL是如何保证主从库数据一致性的?

面试高频:MySQL是如何保证主从库数据一致性的?

作者头像
机智的程序员小熊
发布于 2021-10-27 06:18:17
发布于 2021-10-27 06:18:17
4.6K00
代码可运行
举报
文章被收录于专栏:技术面面观技术面面观
运行总次数:0
代码可运行

介绍

大家好,我是Leo。前面文章我们介绍了WAL的安全机制。可以保证数据的安全性。通过安全性我们分析了binlog,redolog日志的写入机制。今天我们分析一下主从库的实现原理!MySQL是如何保证主从库的数据是一致的呢?

写作思路

根据读者与朋友的反馈,每篇文章我会加一块写作思路。让读者能更好的吸收相关知识,以及判断是否是自己所需要的知识。

主从同步的基本流程

如下图所示,这是主从库的状态图。

  • 状态1:用户端访问MySQLA,A是主库,B是从库,B同步A的数据。
  • 状态2:用户端访问MySQLB,B是主库,A是从库,A同步B的数据。

主从在需要切换的时候就是由状态1转变成状态2的这个过程。

数据在从A同步B或者B同步到A。同步的线程具有超级管理员的权限。所以建议把从库设置成readonly模式的。因为这样可以避免主从同步的一个 “坑” 就是下面的双写。所以设置readonly百利而无一害。

  1. 可以防止其他运营的类的查询语句的误操作。造成数据不一致的问题
  2. 可以防止状态1和状态2在切换的时候也会有一些逻辑性的BUG问题

接下来我们把流程的每一步分析一下,如下图所示

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量
  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

sql_thread线程我们在今后的文章中会详细介绍。这里就不做过多解释了!

根据上面的流程,我们一点一点剖析底层的流程。先来了解一下binlog传输吧

binlog格式的华山论剑

说到binlog传输的话,我们肯定要聊到它的格式问题。binlog常见的格式有两种,一种是statement,一种是row。还有一种格式叫作mixed。这种格式是前面两种格式的混合体。

下面我们举例论述一下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;

insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');

我们先简单的执行一条删除语句,查看一下对应的binlog日志到底是什么样的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mysql> delete from t /*comment*/  where a>=4 and t_modified<='2018-11-10' limit 1;

当binlog格式属于第一种情况时。 statement

binlog里面记录的是SQL语句的原文。可以用 mysql> show binlog events in 'master.000001'; 查看

分析图上的结果。

  • 第一行 SET @@SESSION.GTID_NEXT='ANONYMOUS’你可以先忽略,后面文章我们会在介绍主备切换的时候再提到;
  • 第二行是一个 BEGIN,跟第四行的 commit 对应,表示中间是一个事务;
  • 第三行就是真实执行的语句了。可以看到,在真实执行的 delete 命令之前,还有一个“use ‘test’”命令。这条命令不是我们主动执行的,而是 MySQL 根据当前要操作的表所在的数据库,自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库里,都能够正确地更新到 test 库的表 t。use 'test’命令之后的 delete 语句,就是我们输入的 SQL 原文了。可以看到,binlog“忠实”地记录了 SQL 命令,甚至连注释也一并记录了。
  • 最后一行是一个 COMMIT。你可以看到里面写着 xid=61。

还记得xid是啥意思吗,我们一起回顾一下吧。

xid是binlog与redo log共同的数据字段,崩溃恢复的时候,会按顺序扫描redo log

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

为了说明 statement 和 row 格式的区别,我们来看一下这条 delete 命令的执行效果图

可以看到,这条delete产生了一条warning。是因为当前binlog设置的是statement格式的。并且delete带有limit,很可能会出现主从库数据不一致的情况。比如上面这个例子。

  1. 如果 delete 语句使用的是索引 a,那么会根据索引 a 找到第一个满足条件的行,也就是说删除的是 a=4 这一行;
  2. 但如果使用的是索引 t_modified,那么删除的就是 t_modified='2018-11-09’也就是 a=5 这一行。

由于 statement 格式下,记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a;而在备库执行这条 SQL 语句的时候,却使用了索引 t_modified。因此,MySQL 认为这样写是有风险的。

那么,如果我把 binlog 的格式改为 binlog_format=‘row’, 是不是就没有这个问题了呢?我们先来看看这时候 binog 中的内容吧。

可以看到,与 statement 格式的 binlog 相比,前后的 BEGIN 和 COMMIT 是一样的。但是,row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event:Table_map 和 Delete_rows。

  • Table_map event,用于说明接下来要操作的表是 test 库的表 t;
  • Delete_rows event,用于定义删除的行为。

把格式改成row的话,我们是看不到详细信息的。还需要借助mysqlbinlog工具,用下面这个命令解析和查看binlog中的内容。从上图可以得知,这个事务的binlog是从8900这个位置开始的。所以可以用 start-position 参数来指定从这个位置的日志开始解析。

mysqlbinlog -vv data/master.000001 --start-position=8900;

  • server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
  • 每个 event 都有 CRC32 的值,这是因为我把参数 binlog_checksum 设置成了 CRC32。
  • Table_map event 跟在图 5 中看到的相同,显示了接下来要打开的表,map 到数字 226。现在我们这条 SQL 语句只操作了一张表,如果要操作多张表呢?每个表都有一个对应的 Table_map event、都会 map 到一个单独的数字,用于区分对不同表的操作。
  • 我们在 mysqlbinlog 的命令中,使用了 -vv 参数是为了把内容都解析出来,所以从结果里面可以看到各个字段的值(比如,@1=4、 @2=4 这些值)。
  • binlog_row_image 的默认配置是 FULL,因此 Delete_event 里面,包含了删掉的行的所有字段的值。如果把 binlog_row_image 设置为 MINIMAL,则只会记录必要的信息,在这个例子里,就是只会记录 id=4 这个信息。
  • 最后的 Xid event,用于表示事务被正确地提交了。

你可以看到,当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除 id=4 的行,不会有主备删除不同行的问题。

miexed是啥,给binlog起到了哪些作用

想要解决这个问题, 就需要说明一下row格式的binlog与statement格式的binlog有啥优缺点!

statement 记录的是大概的信息,几乎是我们的执行信息,我们看不到具体的逻辑是什么。所以如果同步到从库上,很容易会发现数据不一致的情况,所以出现了row格式。

row row格式解决了statement的缺点。可以查到执行的详细信息,但是缺点也是相应暴露了出来,过于详细导致内存占用过大。比如删除一个几万的数据。row格式的binlog会记录每个数值记录。这样不仅会占用过多的空间,还会占用磁盘IO,影响整个MySQL的执行效率

miexed 横空出世,解决了statement不一致的问题,同时也解决了row格式的占用内存过大的缺点。主要的实现就是他会判断一下,这个binlog会不会引起数据不一致这个问题。如果会引起,那么久采用row格式的。如果不会引起,那么久采用statement格式的日志。

因此,如果你的线上 MySQL 设置的 binlog 格式是 statement 的话,那基本上就可以认为这是一个不合理的设置。你至少应该把 binlog 的格式设置为 mixed。

比如我们这个例子,设置为 mixed 后,就会记录为 row 格式;而如果执行的语句去掉 limit 1,就会记录为 statement 格式。

接下来,我们就分别从 delete、insert 和 update 这三种 SQL 语句的角度,来看看数据恢复的问题。

如果我执行的是 delete 语句,row 格式的 binlog 也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条 delete 语句以后,发现删错数据了,可以直接把 binlog 中记录的 delete 语句转成 insert,把被错删的数据插入回去就可以恢复了。

如果你是执行错了 insert 语句呢? 那就更直接了。row 格式下,insert 语句的 binlog 里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把 insert 语句转成 delete 语句,删除掉这被误插入的一行数据就可以了。

如果执行的是 update 语句的话,binlog 里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了 update 语句的话,只需要把这个 event 前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了。

其实,由 delete、insert 或者 update 语句导致的数据操作错误,需要恢复到操作之前状态的情况,也时有发生。MariaDB 的Flashback工具就是基于上面介绍的原理来回滚数据的。

案例问题

虽然 mixed 格式的 binlog 现在已经用得不多了,但这里我还是要再借用一下 mixed 格式来说明一个问题,来看一下这条 SQL 语句 mysql> insert into t values(10,10, now());

如果我们把 binlog 格式设置为 mixed,你觉得 MySQL 会把它记录为 row 格式还是 statement 格式呢?

由输出结果得知,走的是statement格式。那如果传给主库同步的话,那里的时间肯定是不准的,造成主从库数据不一致啊。

接下来我们拿xid 用mysqlbinlog工具看一下

这里多了一个指令:SET TIMESTAMP=1546103491 它用 SET TIMESTAMP 命令约定了接下来的 now() 函数的返回时间。

因此,不论这个 binlog 是 1 分钟之后被备库执行,还是 3 天后用来恢复这个库的备份,这个 insert 语句插入的行,值都是固定的。也就是说,通过这条 SET TIMESTAMP 命令,MySQL 就确保了主备数据的一致性。

error: 一定不要用mysqlbinlog工具解析出数据,然后直接把里面的statement语句直接拷贝出来执行。这样的操作是有风险的。所以一定要把整个结构都发给MySQL执行。

主从同步的循环复制问题

在我们真实的开发场景中,往往主库不会一直是主库,从库不会一直是从库。为了保证安全性。往往是这样设计的。

这样的就会出现另一个问题。业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。(我建议你把参数 log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog)。

那么,如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。这个要怎么解决呢?

解决方案:

  1. 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
  2. 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;
  3. 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

按照这个逻辑,如果我们设置了双 M 结构,日志的执行流就会变成这样:

  1. 从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;
  2. 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;
  3. 再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。

总结

这篇文章,我们介绍了MySQL是怎么保证主从库数据一致的原因,实现流程,binlog三种格式的优缺点,线上场景的MySQL主从库应用配置,主从库互相切换的循环复制问题以及解决方案。

知道的越多,不知道的就越多!愿今后的岁月,不忘初心,努力学习!都有一个不辜负的人生!

有任何问题都可以在一起讨论。点赞+评论+关注是对博主最好的支持!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 机智的程序员小熊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JDK自带工具之概览
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/toc.html
IT小马哥
2020/03/18
6980
Java知识理解:为什么安装JDK以及JDK、JRE、JVM三者关系及相关理解
大家都知道电脑的操作系统是由汇编和C语言写出,因此操作系统无法直接识别其他语言。这时我们就需要添加一个(翻译)编译环境,将其他语言(翻译)编译为操作系统能够识别的语言。
鲲志说
2025/04/07
2370
Java知识理解:为什么安装JDK以及JDK、JRE、JVM三者关系及相关理解
JDK,JRE,JVM区别与联系
JVMJDKEclipseJava企业应用 很多朋友可能跟我一样,已经使用JAVA开发很久了,可是对JDK,JRE,JVM这三者的联系与区别,一直都是模模糊糊的。 今天特写此文,来整理下三者的关系。 JDK : Java Development ToolKit(Java开发工具包)。JDK是整个JAVA的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。 最主流的JDK是Sun公司发布的JDK,除了Sun之外,还有很多公司和组织都开发了属于自己的JDK,例如国外IBM公司开发了属于自己的JDK,国 内淘宝也开发了属于自己的JDK,各个组织开发自己的JDK都是为了在某些方面得到一些提高,以适应自己的需求,比如IBM的JDK据说运行效率就比 SUN的JDK高的多。但不管怎么说,我们还是需要先把基础的Sun JDK掌握好。 JDK有以下三种版本: J2SE,standard edition,标准版,是我们通常用的一个版本J2EE,enterpsise edtion,企业版,使用这种JDK开发J2EE应用程序J2ME,micro edtion,主要用于移动设备、嵌入式设备上的java应用程序 我们常常用JDK来代指Java API,Java API是Java的应用程序接口,其实就是前辈们写好的一些java Class,包括一些重要的语言结构以及基本图形,网络和文件I/O等等 ,我们在自己的程序中,调用前辈们写好的这些Class,来作为我们自己开发的一个基础。当然,现在已经有越来越多的性能更好或者功能更强大的第三方类库 供我们使用。 JRE:Java Runtime Enviromental(java运行时环境)。也就是我们说的JAVA平台,所有的Java程序都要在JRE 下才能运行。包括JVM和JAVA核心类库和支持文件。与JDK相比,它不包含开发工具——编译器、调试器和其它工具。 JVM:Java Virtual Mechinal(JAVA虚拟机)。JVM是JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用。Java语言是跨平台运行的,其实就是不同的操作系统,使用不同的JVM映射规则,让其与操作系统无关,完成了跨平台性。JVM 对上层的 Java 源文件是不关心的,它关注的只是由源文件生成的类文件( class file )。类文件的组成包括 JVM 指令集,符号表以及一些补助信息。 下图很好的表面了JDK,JRE,JVM三者间的关系: 我们开发的实际情况是:我们利用JDK(调用JAVA API)开发了属于我们自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些 JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。
Java学习123
2021/12/28
1.6K0
JDK、JRE和JVM三者之间关系
很多程序员已经写了很长一段时间java了,依然不明白JDK,JRE,JVM的区别。今天个人总结一下它们三者的关系、区别。
用户7886150
2020/12/02
6010
Java基础(3)-JDK、JRE、JVM区别与联系
本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/171
joshua317
2021/10/22
3K0
JVM常见面试题(一):JVM是什么、JVM由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别
JVM(Java Virtual Machine,即java虚拟机),java程序的运行环境(java二进制字节码的运行环境)。
寻求出路的程序媛
2024/07/29
1930
JVM常见面试题(一):JVM是什么、JVM由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别
JVM、JRE、JDK的作用与区别
在Java环境配置和项目启动中,这三者的配置是项目启动的基础保证,但这三者的作用和区别呢,本文将对JVM、JRE、JDK的作用与区别进行讲解。
算法与编程之美
2022/05/23
1.3K0
谈一谈|JDK、JRE和JVM的解释以及联系和区别
前要:JDK是 Java 语言的软件开发工具包(SDK)。在JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre。
算法与编程之美
2020/07/16
9350
什么是Java虚拟机以及JDK,JRE,JVM的区别和联系
The Java Virtual Machine (JVM) is an abstract computing machine. The JVM is a program that looks like a machine to the programs written to execute in it. This way, Java programs are written to the same set of interfaces and libraries. Each JVM implementation for a specific operating system, translates the Java programming instructions into instructions and commands that run on the local operating system. This way, Java programs achieve platform independence 翻译一下: Java虚拟机(JVM)是一种抽象计算机器。JVM是一个程序,它看起来像是一台机器,用于编写并在其中执行的程序。通过这种方式,Java程序被写入同一组接口和库中。针对特定操作系统的每个JVM实现都将Java编程指令转换为在本地操作系统上运行的指令和命令。这样,Java程序就实现了平台独立性。
向着百万年薪努力的小赵
2022/12/02
7000
什么是Java虚拟机以及JDK,JRE,JVM的区别和联系
大数据必学Java基础(九):JDK,JRE,JVM的区别
初学JAVA很容易被其中的很多概念弄的傻傻分不清楚,首先从概念上理解一下吧,JDK(Java Development Kit)简单理解就是Java开发工具包,JRE(Java Runtime Enviroment)是Java的运行环境,JVM( java virtual machine)也就是常常听到Java虚拟机。JDK是面向开发者的,JRE是面向使用JAVA程序的用户,上面只是简单的区别
Lansonli
2022/07/05
6720
大数据必学Java基础(九):JDK,JRE,JVM的区别
什么是JVM?什么是JRE?什么是JDK?三者的区别和联系?
总的来说,JDK 是用于 java 程序的开发,而 jre 则是只能运行 class 而没有编译的功能。
bboy枫亭
2020/09/22
1.5K0
什么是JVM?什么是JRE?什么是JDK?三者的区别和联系?
Java简介 | Jdk、Jre、Jvm区别
Jre全称是Java Runtime Environment,意为Java运行环境。
Defu Li
2019/12/19
8810
JDK,JRE,JVM之间的区别和联系
我们写Java代码,用txt就可以写,但是写出来的Java代码,想要运行,需要先编译成字节码,那就需要编译器,而JDK中就包含了编译器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是JVM (Java虚拟机),专门用来执行Java字节码的。
人不走空
2024/02/20
3520
弄懂 JRE、JDK、JVM 之间的区别与联系
其实很多 Java 程序员在写了很多代码后,你问他 jre 和 jdk 之间有什么关系,jvm 又是什么东西,很多人不知所云。本篇不会讲述 jvm 底层是如何与不同的系统进行交互的,而主要理清楚三者之间的区别,搞清楚我们写的 xxx.java 文件是被谁编译,又被谁执行,为什么能够跨平台运行。
Single
2018/03/14
1.4K0
弄懂 JRE、JDK、JVM 之间的区别与联系
JVm JDK JRe 三者区别与联系详解
我将从概念、功能、联系与区别等方面入手,为你详细阐述JVM、JDK和JRE,并结合应用实例辅助理解。
用户4124626
2025/06/12
1040
JVm JDK JRe 三者区别与联系详解
Java基础--JDK的安装和配置弄懂 JRE、JDK、JVM 之间的区别与联系
  Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论允许程序员以优雅的思维方式(思想很重要)进行复杂的编程。Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、动态性特点。Java可以编写桌面应用程序、Web应用程序、分布式系统和嵌入式系统应用程序等。
mukekeheart
2019/09/29
2K0
Java基础--JDK的安装和配置弄懂 JRE、JDK、JVM 之间的区别与联系
Java基础入门笔记01——JAVASE,EE,ME 常用Dos命令,JVM,JRE,JDK「建议收藏」
JavaSE(Java Standard Edition): 标准版,定位在个人计算机上的应用——开发桌面应用(新手入门) JavaEE(Java Enterprise Edition): 企业版,定位在服务器端的应用——建议学习 JavaME(Java Micro Edition): 微型版,定位在消费性电子产品的应用上——不建议学习
全栈程序员站长
2022/09/21
3350
Java基础入门笔记01——JAVASE,EE,ME 常用Dos命令,JVM,JRE,JDK「建议收藏」
【Java那些年系列-启航篇 03】JDK、JRE和JVM之间是什么关系?
Java Development Kit(JDK)是Java编程语言的心脏,为开发者提供了一个完整的开发环境,用于构建、测试、运行和调试Java应用程序。
夏之以寒
2024/04/25
6610
【JAVA-Day02】JDK、JRE和JVM: Java开发与运行的三位好朋友
在Java世界中,JDK、JRE和JVM是三个你需要熟悉的重要角色。它们分别扮演着开发、运行和执行Java程序的不同角色。本文将深入探讨它们之间的关系和作用。
默 语
2024/11/20
1080
【JAVA-Day02】JDK、JRE和JVM: Java开发与运行的三位好朋友
Ubuntu彻底卸载jdk「建议收藏」
1、移除所有java相关的包(sun,Oracle, openJDK, lcedTea plugins ,GIJ)
全栈程序员站长
2022/09/07
5.2K0
Ubuntu彻底卸载jdk「建议收藏」
推荐阅读
相关推荐
JDK自带工具之概览
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验