MGR基本使用(转)
第一章:MGR介绍
MGR(MySQL Group Replication)是一个MySQL Server插件,可用于创建弹性,高可用MySQL集群方案。有一个内置的组成员服务,在任何给定的时间点,保持组的视图一致并可供所有服务器使用。服务器可以离开并加入组,视图也会相应更新。当成员离开组,故障检测机制会检测到此情况并通知组视图已更改。
1.1传统的数据主从复制
1.1.1主从复制
在这里插入图片描述
传统的数据主从辅助属于异步复制,从库起IO线程连接主库,获取主库二进制日志写到本地中继日志,并更新master-info文件(存放主库相关信息),从库再利用SQL线程执行中继日志。
1.1.2半同步复制
在这里插入图片描述
半同步复制是建立在基本的主从复制基础上,利用插件完成半同步复制,传统的主从复制,不管从库是否正确获取到二进制日志,主库不断更新,半同步复制则当确认了从库把二进制日志写入中继日志才会允许提交,如果从库迟迟不返回ack,主库会自动将半同步复制状态取消,进入最基本的主从复制模式。
1.2 组复制
在这里插入图片描述
组复制是一种可用于实现容错系统的技术。复制组是一个通过消息传递相互交互的server集群。复制组由多个server成员组成,如上图的master1,master2,master3,所有成员独立完成各自的事务。当客户端先发起一个更新事务,该事务先在本地执行,执行完成之后就要发起对事务的提交操作了。在还没有真正提交之前需要将产生的复制写集广播出去,复制到其他成员。如果冲突检测成功,组内决定该事务可以提交,其他成员可以应用,否则就回滚。最终,这意味着所有组内成员以相同的顺序接收同一组事务。因此组内成员以相同的顺序应用相同的修改,保证组内数据强一致性。
新成员加入组的简单流程:
当有新的成员加入组中,组内原有的成员会在二进制日志中插入一个视图切换的事件。
在组成员内找到一个donor捐赠之前缺失的数据,如果这个donor突然下线了,新成员会从新的donor获取缺失的数据,这时候组还在不断更新,新成员会将新的事件写到内存的一个临时空间
当获取到视图切换事件的时候,新成员将开始执行保存到内存临时空间的事件
1.3工作模式
单主模式
只有一个server成员接受更新
多主模式
所有的server成员同时接受更新
第二章:MGR部署
2.1前期规划
IP 主机名 数据库版本信息 server-id
192.168.240.203 MGR_NODE1 MySQL-5.7.25 203
192.168.240.204 MGR_NODE2 MySQL-5.7.25 204
192.168.240.205 MGR_NODE3 MySQL-5.7.25 205
2.2 环境准备
关闭防火墙以及selinux(所有机器执行)
# 关闭防火墙
systemctl stop firewalld
# 临时关闭selinux
setenforce 0
1
2
3
4
配置主机名,按照先前规划,填写/etc/hosts文件
[root@localhost data]# cat /etc/hosts
192.168.240.203 mgr_node1
192.168.240.204 mgr_node2
192.168.240.205 mgr_node3
1
2
3
4
修改本机主机名(每台机根据情况修改自己主机名)
hostnamectl set-hostname mgr_node1
1
2.3 安装MySQL5.7.25
在与本文档同级目录提供了MySQL5.7.25的rpm包,上传到所有服务器并执行下列命令
# 解压安装包
tar xf mysql-community-5.7.25-rpm.tgz
# 安装依赖包
yum install net-tools -y
# 卸载mariadb-libs包
yum remove mariadb-libs -y
# 安装MySQL
cd repo.mysql.com/
rpm -ivh ./*
1
2
3
4
5
6
7
8
9
初始化数据库
# 创建存放数据库初始化信息的目录(所有机器都要执行)
mkdir -pv /data/{data,log,temp}
# 修改数据目录属主属组
chown -R mysql:mysql /data
# 初始化数据库,设置默认密码为空
mysqld --initialize-insecure --datadir=/data/data --user=mysql
# 修改数据目录属主属组
chown -R mysql:mysql /data
1
2
3
4
5
6
7
8
查看一下初始化之后的数据目录结构
[root@localhost repo.mysql.com]# tree /data/ -L 1
/data/
├── data # 存放MySQL数据信息
├── log # 存放MySQL产生的错误日志,二进制日志信息
└── temp # 存放MySQL的一些临时文件,比如pid,sock文件
1
2
3
4
5
数据目录如此设计,只是为了便于管理
2.4生成MGR组名
在MGR_NODE1节点上修改MySQL配置文件/etc/my.cnf
[mysqld]
user=mysql
port=3306
datadir=/data/data
log-error=/data/log/err.log
pid-file=/data/temp/mysqld.pid
socket=/data/temp/mysql.sock
symbolic-links=0
[client]
socket=/data/temp/mysql.sock
1
2
3
4
5
6
7
8
9
10
11
在MGR_NODE1节点上启动MySQL
mysqld --defaults-file=/etc/my.cnf --daemonize
1
在MGR_NODE1节点上修改数据库密码
# 修改root密码,执行命令之后输入您要设置的root密码
mysqladmin -uroot password
1
2
在MGR_NODE1节点上生成一个UUID,作为MGR组名
# 执行后输入密码
[root@localhost repo.mysql.com]# mysql -uroot -p -e "SELECT UUID()"
Enter password:
+--------------------------------------+
| UUID() |
+--------------------------------------+
| eb5d4f12-5f2f-11e9-8a72-000c29108615 |
+--------------------------------------+
1
2
3
4
5
6
7
8
组名为eb5d4f12-5f2f-11e9-8a72-000c29108615
再次修改MGR_NODE1配置文件/etc/my.cnf文件
[mysqld]
user=mysql
port=3306
datadir=/data/data
log-error=/data/log/err.log
pid-file=/data/temp/mysqld.pid
socket=/data/temp/mysql.sock
symbolic-links=0
server_id=203
gtid_mode=ON
enforce_gtid_consistency=ON
master_info_repository=TABLE
relay_log_info_repository=TABLE
binlog_checksum=NONE
log_slave_updates=ON
log_bin=/data/log/binlog
binlog_format=ROW
transaction_write_set_extraction=XXHASH64
loose-group_replication_group_name="eb5d4f12-5f2f-11e9-8a72-000c29108615"
loose-group_replication_start_on_boot=off
loose-group_replication_local_address= "192.168.240.203:13306"
loose-group_replication_group_seeds= "192.168.240.203:13306,192.168.240.204:13306,192.168.240.205.13306"
loose-group_replication_bootstrap_group=off
loose-group_replication_single_primary_mode=off
loose-group_replication_enforce_update_everywhere_checks=true
[client]
socket=/data/temp/mysql.sock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
以上标准版文件需要更改的点有:
server_id:不能与其他MySQL实例重复
loose-group_replication_group_name:通过SELECT UUID()生成一个UUID作为组名
loose-group_replication_local_address:当前节点用于内部通讯的ip:port
loose-group_replication_group_seeds:组内成员通讯的ip:port
参数讲解:
基本参数 描述
user 启动进程的user
port 数据库使用的端口
datadir 数据库的数据目录位置
log-error 数据库的错误日志位置
pid-file 数据库的pid文件位置
socket 数据库的sock文件位置
symbolic-links 禁用符号链接以防止出现各种安全风险
MGR要求的相关参数 描述
server_id 不同实例必须保证此server_id不同,如果启用了二进制日志记录,则必须指定该选项,否则不允许服务器启动
gtid_mode 使用全局事务标识符(GTID)来标识事务。将此选项设置为–gtid-mode=ON 要求 enforce-gtid-consistency设置为ON
enforce_gtid_consistency ON:不允许任何事务违反GTID一致性 OFF:允许事务违反GTID一致性。WARN:允许所有事务违反GTID一致性,但在这种情况下会生成警告
master_info_repository 设置从站将主状态和连接信息记录到 FILE(master.info)还是TABLE (mysql.slave_master_info)中
relay_log_info_repository 设置从站在中继日志中的位置是写入FILE (relay-log.info)还是 写入TABLE (mysql.slave_relay_log_info)中
binlog_checksum 启用后,此变量会使主服务器为二进制日志中的每个事件写入校验和,当binlog_checksum禁用(值 NONE)时,服务器通过编写和检查每个事件的事件长度(而不是校验和)来验证它是否只将完整事件写入二进制日志
log_slave_updates 设置从主服务器接受的更新是否写入二进制日志中
log_bin 设置二进制日志的位置
binlog_format 二进制日志格式,有行模式,语句模式,混合模式,使用MGR必须使用行模式
组复制相关参数 描述
transaction_write_set_extraction 定义用于生成标识与事务关联的写入的哈希的算法,哈希值将用于分布式冲突检测和处理
loose-group_replication_group_name 通知插件它正在加入或创建的组,需要使用SELECT UUID()生成一个UUID
loose-group_replication_start_on_boot 指示插件在服务器启动时不自动引导组操作
loose-group_replication_local_address 诉插件使用哪个ip:port与组中的其他成员进行内部通信。这里的ip与端口不能与MySQL提供的ip:port 相同,如果使用相同ip则port必须不相同
loose-group_replication_group_seeds 设置组成员的主机名和端口
loose-group_replication_bootstrap_group 插件是否引导组,此选项只能在任何时候在一个服务器实例上使用,通常是第一次引导组时(或者在整个组关闭并重新备份的情况下)。如果多次引导组,例如当多个服务器实例设置了此选项时,则可以创建一个人工分裂脑情景,其中存在两个具有相同名称的不同组。
loose-group_replication_single_primary_mode 单主模式设置为ON,多主模式设置为OFF
loose-group_replication_enforce_update_everywhere_checks 在所有节点启用多主数据更新的严格一致性检查
重启数据库
# 关闭数据库,执行后输入root密码
mysqladmin -uroot -p shutdown
# 启动数据库
mysqld --defaults-file=/etc/my.cnf --daemonize
1
2
3
4
2.5启动首个MGR节点
# 登陆数据库
mysql -uroot -p
1
2
以下命令是在数据库中执行
# 创建MGR内部消息互通的用户
SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'Hal@123';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
# 安装MGR插件
INSTALL PLUGIN group_replication SONAME 'group_replication.so';
# 检查是否成功安装
SHOW PLUGINS ;
# 使用当前数据库节点引导组,一个组内只有一个节点能引导
# 不允许组内多次引导,这将导致产生两个相同名称的不同组
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
查看是否成功
mysql> SELECT * FROM performance_schema.replication_group_members\G
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
MEMBER_ID: 136fb2a3-5e5b-11e9-bc99-000c2990d255
MEMBER_HOST: MGR_NODE1
MEMBER_PORT: 3306
MEMBER_STATE: ONLINE
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
MEMBER_STATE显示为ONLINE表示成功
2.6让其余节点加入组
从第一个节点复制/etc/my.cnf文件到其他主机进行修改
[mysqld]
user=mysql
port=3306
datadir=/data/data
log-error=/data/log/err.log
pid-file=/data/temp/mysqld.pid
socket=/data/temp/mysql.sock
symbolic-links=0
server_id=203
gtid_mode=ON
enforce_gtid_consistency=ON
master_info_repository=TABLE
relay_log_info_repository=TABLE
binlog_checksum=NONE
log_slave_updates=ON
log_bin=/data/log/binlog
binlog_format=ROW
transaction_write_set_extraction=XXHASH64
loose-group_replication_group_name="eb5d4f12-5f2f-11e9-8a72-000c29108615"
loose-group_replication_start_on_boot=off
loose-group_replication_local_address= "192.168.240.203:13306"
loose-group_replication_group_seeds= "192.168.240.203:13306,192.168.240.204:13306,192.168.240.205.13306"
loose-group_replication_bootstrap_group=off
loose-group_replication_single_primary_mode=off
loose-group_replication_enforce_update_everywhere_checks=true
[client]
socket=/data/temp/mysql.sock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
修改的点如下
server_id修改为前期部署规划的值
loose-group_replication_local_address修改为该节点用于内部通讯的ip地址
启动数据库
mysqld --defaults-file=/etc/my.cnf --daemonize
1
设置数据库root密码
mysqladmin -uroot password
1
进入数据库
mysql -uroot -p
1
以下操作在数据库中执行
# 创建MGR内部消息互通的用户
SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'Hal@123';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
# 安装MGR插件
INSTALL PLUGIN group_replication SONAME 'group_replication.so';
# 加入组复制
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='Hal@123' FOR CHANNEL 'group_replication_recovery';
START GROUP_REPLICATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
检查是否成功,数据库中执行
SELECT * FROM performance_schema.replication_group_members
1
MEMBER_STATE字段都为ONLINE即可
测试
在某个节点的数据库内执行
CREATE DATABASE test;
USE test;
CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 TEXT NOT NULL);
INSERT INTO t1 VALUES (1, 'Luis');
1
2
3
4
在所有节点数据库内查看是否有这条数据
mysql> SELECT * FROM test.t1;
+----+------+
| c1 | c2 |
+----+------+
| 1 | Luis |
+----+------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
当所有节点都显示这条数据,则证明搭建成功
第三章:MGR监控
3.1performance_schema.replication_group_members
组成员信息
mysql> SELECT * FROM performance_schema.replication_group_members;
+---------------------------+--------------------------------------+-------------+-------------+--------------+
| CHANNEL_NAME | MEMBER_ID | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE |
+---------------------------+--------------------------------------+-------------+-------------+--------------+
| group_replication_applier | b956519d-5e82-11e9-aa61-000c291a4b91 | MGR_NODE3 | 3306 | ONLINE |
| group_replication_applier | bb4d96d6-5e7c-11e9-829d-000c296df978 | MGR_NODE2 | 3306 | ONLINE |
| group_replication_applier | e79b0376-5e6f-11e9-8d47-000c2990d255 | MGR_NODE1 | 3306 | ONLINE |
+---------------------------+--------------------------------------+-------------+-------------+--------------+
3 rows in set (0.00 sec)
1
2
3
4
5
6
7
8
9
MEMBER_STATE 描述
ONLINE 该成员已准备好,这意味着客户端可以连接并开始执行事务
RECOVERING 该成员正在积极成为组成员,目前正在进行恢复过程,从donor那里接收状态信息
OFFLINE 插件已加载但该成员不属于任何组
ERROR 恢复阶段或应用更改时出现错误,服务器就会进入此状态。
UNREACHABLE 每当本地故障检测器怀疑某个服务器无法访问时,例如它被非自愿断开,它就会显示服务器的状态为UNREACHABLE。
CHANNEL_NAME 描述
group_replication_recovery 此通道用于与分布式恢复阶段相关的复制更改。
group_replication_applier 此通道用于接受更新
3.2performance_schema.replication_group_member_stats
组成员状态信息
mysql> SELECT * FROM performance_schema.replication_group_member_stats\G
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
VIEW_ID: 15553045738118081:5
MEMBER_ID: 48888dd5-5f3b-11e9-9b62-000c2990d255
COUNT_TRANSACTIONS_IN_QUEUE: 0
COUNT_TRANSACTIONS_CHECKED: 4
COUNT_CONFLICTS_DETECTED: 0
COUNT_TRANSACTIONS_ROWS_VALIDATING: 0
TRANSACTIONS_COMMITTED_ALL_MEMBERS: eb5d4f12-5f2f-11e9-8a72-000c29108615:1-8
LAST_CONFLICT_FREE_TRANSACTION: eb5d4f12-5f2f-11e9-8a72-000c29108615:7
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
参数 描述
CHANNEL_NAME 通道名称
VIEW_ID 事务ID,前缀部分:组初始化时产生,为当时的时间戳,组存活期间该值不会发生变化。所以,该字段可用于区分2个视图是否为同一个组的不同时间点;后缀部分:每次视图更改会触发一次更改,从1开始单调递增
MEMBER_ID 实例的UUID,每个成员都有不同的值
COUNT_TRANSACTIONS_IN_QUEUE 待处理冲突检测的队列中的事务数。一旦检查了事务的冲突,如果它们通过了检查,它们就会排队等待提交
COUNT_TRANSACTIONS_CHECKED 已检查冲突的事务数
COUNT_CONFLICTS_DETECTED 未通过冲突检测检查的事务数
COUNT_TRANSACTIONS_ROWS_VALIDATING 用于认证但未进行垃圾回收的事务数量
TRANSACTIONS_COMMITTED_ALL_MEMBERS 已在复制组的所有成员上成功提交的事务
LAST_CONFLICT_FREE_TRANSACTION 已检查的最后一个无冲突事务的事务标识符。
3.3performance_schema.replication_connection_status
显示有关组复制的信息,例如,已从组接收并在应用程序队列中排队的事务(中继日志)
mysql> SELECT * FROM performance_schema.replication_connection_status\G
*************************** 1. row ***************************
CHANNEL_NAME: group_replication_applier
GROUP_NAME: eb5d4f12-5f2f-11e9-8a72-000c29108615
SOURCE_UUID: eb5d4f12-5f2f-11e9-8a72-000c29108615
THREAD_ID: NULL
SERVICE_STATE: ON
COUNT_RECEIVED_HEARTBEATS: 0
LAST_HEARTBEAT_TIMESTAMP: 0000-00-00 00:00:00
RECEIVED_TRANSACTION_SET: eb5d4f12-5f2f-11e9-8a72-000c29108615:1-8
LAST_ERROR_NUMBER: 0
LAST_ERROR_MESSAGE:
LAST_ERROR_TIMESTAMP: 0000-00-00 00:00:00
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
参数 描述
CHANNEL_NAME 通道名称
GROUP_NAME 组名
SOURCE_UUID 源UUID
THREAD_ID I/O线程ID号
SERVICE_STATE ON(线程存在且处于活动状态或空闲状态),OFF(线程不再存在)或CONNECTING(线程存在且正在连接到主服务器)
COUNT_RECEIVED_HEARTBEATS:
LAST_HEARTBEAT_TIMESTAMP 从库自上次重新启动或重置或发出CHANGE MASTER TO语句以来收到的心跳信号总数
RECEIVED_TRANSACTION_SET 从库接收的所有事务对应的GTID集
LAST_ERROR_NUMBER 导致I / O线程停止的最新错误的错误号
LAST_ERROR_MESSAGE 导致I / O线程停止的最新错误的错误消息
LAST_ERROR_TIMESTAMP YYMMDD HH:MM:SS格式 的时间戳,显示最近的I / O错误发生的时间
3.4 performance_schema.replication_applier_status
显示与组复制相关的通道和线程的状态如果有许多不同的工作线程应用事务,那么工作表也可用于监视每个工作线程正在执行的操作
mysql> SELECT * FROM performance_schema.replication_applier_status;
+---------------------------+---------------+-----------------+----------------------------+
| CHANNEL_NAME | SERVICE_STATE | REMAINING_DELAY | COUNT_TRANSACTIONS_RETRIES |
+---------------------------+---------------+-----------------+----------------------------+
| group_replication_applier | ON | NULL | 0 |
+---------------------------+---------------+-----------------+----------------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
3.5 mysql.slave_relay_log_info
存放从库relay log 信息
mysql> select * from mysql.slave_relay_log_info\G
*************************** 1. row ***************************
Number_of_lines: 7
Relay_log_name: ./mgr_node1-relay-bin-group_replication_applier.000002
Relay_log_pos: 1941
Master_log_name:
Master_log_pos: 180
Sql_delay: 0
Number_of_workers: 0
Id: 1
Channel_name: group_replication_applier
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
3.6mysql.slave_master_info
存放master相关信息
mysql> select * from mysql.slave_master_info\G
*************************** 1. row ***************************
Number_of_lines: 25
Master_log_name:
Master_log_pos: 4
Host: <NULL>
User_name:
User_password:
Port: 0
Connect_retry: 60
Enabled_ssl: 0
Ssl_ca:
Ssl_capath:
Ssl_cert:
Ssl_cipher:
Ssl_key:
Ssl_verify_server_cert: 0
Heartbeat: 30
Bind:
Ignored_server_ids: 0
Uuid:
Retry_count: 86400
Ssl_crl:
Ssl_crlpath:
Enabled_auto_position: 1
Channel_name: group_replication_applier
Tls_version:
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
第四章:组复制要求与限制
4.1要求
InnoDB存储引擎。数据必须存储在 InnoDB事务存储引擎中。事务以乐观方式执行,然后在提交时检查冲突。如果存在冲突,为了保持整个组的一致性,将回滚一些事务。这意味着需要事务存储引擎。此外, InnoDB还提供了一些附加功能,可以在与Group Replication一起操作时更好地管理和处理冲突。
主键。必须使用拥有主键或者等效主键,也就是唯一且非空键
IPv4网络。 MySQL Group Replication使用的组通信引擎仅支持IPv4。
网络性能。 MySQL Group Replication旨在部署在服务器实例彼此非常接近的集群环境中。网络延迟和网络带宽都会影响组的性能和稳定性。所有小组成员之间必须始终保持双向通信。如果阻止服务器实例的入站或出站通信(例如,通过防火墙或连接问题),则该成员无法在该组中运行,并且组成员(包括有问题的成员)可能无法报告受影响的服务器实例的正确成员状态
4.2限制
启动二进制日志。 设置 --log-bin[=log_file_name]。MySQL Group Replication会复制二进制日志内容,因此二进制日志需要打开才能运行。
二进制日志行格式。 设置--binlog-format=row,查看行格式的二进制日志可以用mysqlbinlog --base64-output='DECODE-ROWS' /data/log/binlog.000002或者在数据库内执行``
从主库获取的日志提交之后记录到本地binlog日志中,设置--log-slave-updates。
启用GTID,设置--gtid-mode=ON
设置--master-info-repository=TABLE 和 --relay-log-info-repository=TABLE。复制应用程序需要将master信息和中继日志元数据写入系统表mysql.slave_master_info和 mysql.slave_relay_log_info系统表。这可确保组复制插件具有一致的可复制性和复制元数据的事务管理
设置 --transaction-write-set-extraction=XXHASH64此标记用于检测冲突。
多线程并行执行事务,相关参数如下
参数 描述
–slave-parallel-workers=N 并发执行的线程数,0为默认值,单线程模式,并行应用事务可能是无序的因此需要slave_preserve_commit_order=1
slave_preserve_commit_order 对于单线程执行事务的模式,这个选项不起作用,对于多线程模式,启用这个选项可以确保事务执行的顺序与从站中继日志中顺序相同,如果设置为1时,slave_parallel_type=LOGICAL_CLOCK
slave_parallel_type 并行复制的模式。默认DATABASE,表示库级别的并行复制;LOGICAL_CLOCK:基于组提交的并行复制方式
参考文档
https://dev.mysql.com/doc/refman/5.7/en/server-system-variable-reference.html
http://mysql.taobao.org/monthly/2017/08/01/
https://zhuanlan.zhihu.com/p/40627399
https://yq.aliyun.com/articles/571616/
————————————————
版权声明:本文为CSDN博主「十五十六」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/L835311324/article/details/89345195