mongo数据备份
1. mongo单机安装
mongo版本:4.4.19
服务器版本:CentOS Linux release 7.9.2009
cd /etc/yum.repos.d/
touch mongodb-org-4.4.repo
[mongodb-org-4.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc
1
2
3
4
5
6
yum install -y mongodb-org
systemctl start mongod
卸载mongo:
1、在终端命令行中输入以下命令停止服务。
sudo service mongod stop
2、再输入命令删除软件包。
sudo yum erase $(rpm -qa | grep mongodb-org)
3、通过rm命令删除mongodb相关文件
sudo rm -r /var/log/mongodb
sudo rm -r /var/lib/mongo
1
2
3
4
5
6
7
2. mongo(replica set)部署
1.无密码副本集部署
在三台机器按上述方法安装mongo:
192.168.91.12:27017
192.168.91.13:27017
192.168.91.14:27017
各个mongo节点修改mongo配置以副本集模式运行:
vim /etc/mongod.conf
replication:
replSetName: "rs0"
1
2
shell 登录mongo primary节点执行:
rs.initiate({
"_id": "rs0",
"members": [
{ "_id": 0, "host": "192.168.91.12:27017" },
{ "_id": 1, "host": "192.168.91.13:27017" },
{ "_id": 2, "host": "192.168.91.14:27017" }
]
});
1
2
3
4
5
6
7
8
执行后,12为primary节点, 13,14为secondary节点,
写入操作只能再primary节点,secondary节点不能进行写入操作,且会自动同步primary节点数据,若primary节点down机,secondary节点会自动选举出一个新的primary节点,此操作完全由mongo自动进行,不依赖三方插件。
副本集 移除节点命令:
rs.remove("192.168.91.13:27017");
rs.remove("192.168.91.14:27017");
1
2
2.单机密码认证mongo改造单点Primary
openssl rand -base64 90 -out /etc/keyfile
chmod 400 /etc/keyfile
chown mongod /etc/keyfile
1
2
3
4
vim /etc/mongod.conf
security:
authorization: enabled
keyFile: /etc/keyfile
replication:
replSetName: "rs0"
1
2
3
4
5
6
systemctl restart mongod
进入mongo shell:
use admin;
db.auth("root","pwd");
rs.initiate({
"_id": "rs0",
"members": [
{ "_id": 0, "host": "192.168.0.11:27017" }
]
})
1
2
3
4
5
6
7
8
9
3. mongodump 与 mongorestore工具使用
mongodump的使用
备份所有库
sudo mongodump --gzip --out /var/backups/mongo
1
备份整个数据库
sudo mongodump --db mydb --out /var/backups/mongo
1
–db 数据库名
–out 备份文件输出文件夹
备份单个表
mongodump -d mydb -o /var/backups/mongo --collection users
1
-d 数据库
-o 备份文件输出路径
–collection 指定的备份表
mongorestore的使用
恢复所有数据库
mongorestore -h 127.0.0.1 -p 27017 --username root2 --password 123456 /data/backup/mongodb_all_backup/mongo
1
恢复整个数据库
sudo mongorestore --db mydb /var/backups/mongo/mydb
1
恢复单个集合
mongorestore -d mydb -c users mydb/users.bson
1
4.rsync工具使用
Linux下的远程数据同步工具,通过网络同步多台主机间的文件和目录。
安装:
yum install rsync -y
1
rsync运行模式:C/S
rsync监听端口:873
服务端配置
vim /etc/rsyncd.conf
uid = root
gid = root
port = 873
fake super = yes
use chroot = no
max connections = 200
timeout = 600
ignore errors
read only = false
list = false
auth users = rsync_backup
secrets file = /etc/rsync.passwd
pid file = /var/run/rsyncd.pid
address = 192.168.91.215
hosts allow = *
hosts deny = *
[backup]
comment = im rsync update dir
path = /data/backup
[mongodb_all_backup]
path = /data/backup/mongodb_all_backup
[mongodb_incr_backup]
path = /data/backup/mongodb_incr_backup
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
vim /etc/rsync.passwd
rsync_backup:yfk123456
1
systemctl restart rsyncd
客户端配置
vim /etc/rsync.pass
yfk123456
1
客户端推送与拉取文件
push
rsync -avz file.tar.gz rsync_backup@192.168.91.215::mongodb_all_backup/ --password-file=/etc/rsync.pass
1
命令参数解析:
rsync_backup: 服务端rsync用户名
192.168.91.215: 服务端地址
mongodb_all_backup/: 服务端创建文件模块
–password-file: 认证用户密码文件
pull
rsync -avz rsync_backup@192.168.91.215::mongodb_all_backup/ /data/ --password-file=/etc/rsync.pass
1
命令参数说明:
a#归档模式传输, 等于-tropgDl
-v #详细模式输出, 打印速率, 文件数量等
-z #传输时进行压缩以提高效率
-r #递归传输目录及子目录,即目录下得所有目录都同样传输。
-t #保持文件时间信息
-o #保持文件属主信息
-p #保持文件权限
-g #保持文件属组信息
-l#保留软连接
-P #显示同步的过程及传输时的进度等信息
-D #保持设备文件信息
-L #保留软连接指向的目标文件
-e#使用的信道协议,指定替代rsh的shell程序
--exclude=PATTERN #指定排除不需要传输的文件模式
--exclude-from=file #文件名所在的目录文件
--bwlimit=100#限速传输
--partial #断点续传
--delete #让目标目录和源目录数据保持一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5. 完整mongo全量备份脚本
vim mongodb_all_back.sh
#!/bin/bash
#############################################################################
# 此脚本为 mongodb 的全量备份脚本,配合增量备份脚本一起使用 #
# 备份策略为:每周一次完全备份(0 4 * * 1),每天一次增量备份(30 3 * * *)。 #
#############################################################################
#推送备份服务器设置
backupServerHost='192.168.91.215'
backupModule='mongodb_all_backup'
backupUser='rsync_backup'
#客户端密码文件位置: /etc/rsync.pass chmod 600 /etc/rsync.pass
#mongo 数据库设置
host='192.168.91.12'
port='27017'
sourcepath=''
nowtime=$(date "+%Y%m%d")
#### 自动检查/创建增量备份路径 ####
if [ ! -d /data/mongodb_back/mongodb_backup_all/mongo-$port ];then
mkdir -p /data/mongodb_back/mongodb_backup_all/mongo-$port
fi
if [ ! -d /data/mongodb_back/mongodb_backup_all/log-$port ];then
mkdir -p /data/mongodb_back/mongodb_backup_all/log-$port
fi
targetpath="/data/mongodb_back/mongodb_backup_all/mongo-$port"
logpath="/data/mongodb_back/mongodb_backup_all/log-$port"
#### 备份函数 ####
start(){
mongodump --host $host --port $port --oplog --gzip --out ${targetpath}/${nowtime}
}
#### 判断备份是否成功的函数 ####
execute(){
echo "=========================$(date) backup all mongodb back start ${nowtime}=========" >> ${logpath}/${nowtime}.log
start
if [ $? -eq 0 ];then
echo "The MongoDB BackUp Successfully!" >> ${logpath}/${nowtime}.log
cd ${targetpath}
tar -zcvf "${nowtime}.tar.gz" ${nowtime}
echo "打包压缩完成 ${targetpath}/${nowtime}.tar.gz" >> ${logpath}/${nowtime}.log
#推送数据库备份文件 到备份服务器
echo "Message --传输数据库备份文件到备份服务器:" >> ${logpath}/${nowtime}.log
rsync -avz ${nowtime}.tar.gz ${backupUser}@${backupServerHost}::${backupModule}/ --password-file=/etc/rsync.pass >> ${logpath}/${nowtime}.log
#删除备份文件夹
rm -rf ${nowtime}/
else
echo "The MongoDB BackUp Failure" >> ${logpath}/${nowtime}.log
fi
}
execute
#### 删除7天前的备份 ####
backtime=$(date -d '-7 days' "+%Y%m%d")
if [ -f "${targetpath}/${backtime}.tar.gz" ];then
rm -rf "${targetpath}/${backtime}.tar.gz"
echo "=======${targetpath}/${backtime}/===删除完毕==" >> ${logpath}/${nowtime}.log
fi
echo "========================= $(date) backup all mongodb back end ${nowtime}=========" >> ${logpath}/${nowtime}.log
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
执行
sh /data/mongodb_back/script/mongodb_all_back.sh
输出:
数据库服务器:
备份后压缩包直接通过rsync发送到备份服务器:
全量备份数据文件位置:
/data/mongodb_back/mongodb_backup_all/mongo-27017
恢复全量备份数据
先登录mongo, 创建执行 mongorestore命令的专属权限用户root2:
use admin;
db.createRole(
{
role: "interalUseOnlyOplogRestore",
privileges: [
{ resource: { anyResource: true }, actions: [ "anyAction" ] }
],
roles: []
}
);
db.createUser({
user: "root2",
pwd: "123456",
roles: [
"interalUseOnlyOplogRestore"
]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行命令:
先解压tar.gz
cd /data/mongodb_back/mongodb_backup_all/mongo-27017/20230227
tar zxvf 20230227.tar.gz
1
2
再执行恢复命令
mongorestore -h 127.0.0.1 -p 27017 --username root2 --password 123456 --gzip /data/mongodb_back/mongodb_backup_all/mongo-27017/20230227
1
6. 完整mongo增量备份脚本(基于oplog)
oplog使用固定大小集合记录了数据库中所有修改操作的操作日志(新增、修改和删除,无查询),mongodb收到修改请求后,先在主节点(Primary)执行请求,再把操作日志保存到oplog表中,其他从节点(Secondary)到主节点拉取oplog并在异步进程中应用这些操作,从而达到主从数据的一致性。复制组内的所有节点都会保存一份oplog(集合名local.oplog.rs),这让他们可以保持同样的数据库状态。
mongodump 中的一个选项 --query 可以限制 mongodump 备份的内容, --oplogReplay 会在还原数据库转储后,还会从转储目录顶层中的 oplog.bson 文件中执行 oplog 条目。oplog 中又滚动记录这关于所有数据库写操作的日志。那么我们就可以通过备份 oplog 来实现增量备份。
通过限制oplog记录的 ts字段(时间戳),可达到秒级别的增量备份。
设置查询条件: 小于当前时间时间戳, 大于当前时间-1天时间戳可进行天级别的备份。
-query 参数:
{\"ts\":{\"\$gte\":{\"\$timestamp\": {\"t\": $ParamBakStartDate, \"i\": 1}},\"\$lte\":{\"\$timestamp\": {\"t\": $ParamBakEndDate, \"i\": 9999}}}}
1
由于shell脚本 $符号 和mongo的查询关键字 $gte冲突,所以将查询mongo查询语句输出到文件,通过
–queryFile参数查询
echo "{\"ts\":{\"\$gte\":{\"\$timestamp\": {\"t\": $ParamBakStartDate, \"i\": 1}},\"\$lte\":{\"\$timestamp\": {\"t\": $ParamBakEndDate, \"i\": 9999}}}}" > $bkdatapath/query.js
1
参考资料:MongoDB Developer Community
使用 db.printReplicationInfo() 查看oplog时间:
查询oplog记录:
use local;
db.oplog.rs.find();
db.oplog.rs.find({"ts":{"$gte":Timestamp(1677113401,1),"$lte":Timestamp(1677200401,9999)}});
1
2
3
oplog 数据格式:
{
"ts": Timestamp(1625660877, 2), // 日志的操作时间戳,第一个数字是时间戳,单位秒,第二个数字是当前秒的第2个操作
"t": NumberLong(2),
"h": NumberLong("5521980394145765083"),
"v": 2,
"op": "i", // i表示insert,u表示update,d表示delete,c 表示的是数据库的命令,比如建表,n表示noop,即空操作
"ns": "test.users", // 命名空间,即数据库和集合名称
"ui": UUID("edabbd93-76eb-42be-b54a-cdc29eb1f267"), // 连接到mongodb的客户端会话id
"wall": ISODate("2021-07-07T12:27:57.689Z"), // 操作执行时间,utc时间
"o": { // 操作的内容,对于不同的op类型,其格式不尽相同
"_id": ObjectId("60e59dcd46db1fb4605f8b18"),
"name": "1"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim mongodb_incr_back.sh
#!/bin/bash
#############################################################################
# 此脚本为 mongodb 的增量备份脚本,配合完全备份脚本一起使用 #
# 备份策略为:每周一次完全备份(0 4 * * 1) 每周一凌晨4点,每天一次增量备份(30 3 * * *)。每天凌晨3点半 #
#############################################################################
# 每 10分钟执行一次 */10 * * * *
# 每 小时 执行一次 * */1 * * *
#
#推送备份服务器设置
backupServerHost='192.168.91.215'
backupModule='mongodb_incr_backup'
backupUser='rsync_backup'
#客户端密码文件位置: /etc/rsync.pass chmod 600 /etc/rsync.pass
#mongo服务器设置
command_linebin="mongo"
port="27017"
#### 自动检查/创建增量备份路径 ####
if [ ! -d "/data/mongodb_back/mongodboplog_back/mongo-$port" ];then
mkdir -p /data/mongodb_back/mongodb_oplog_back/mongo-$port
fi
if [ ! -d "/data/mongodb_back/mongodboplog_back/log-$port" ];then
mkdir -p /data/mongodb_back/mongodb_oplog_back/log-$port
fi
bkdatapath=/data/mongodb_back/mongodb_oplog_back/mongo-$port
bklogpath=/data/mongodb_back/mongodb_oplog_back/log-$port
#### 获取增量(oplog)备份的起止时间 ####、
#### 变量 DiffTime 为备份间隔,我这里是24小时+10分钟。多出来的10分钟是预估每次备份时间在10分钟以内,来确保数据的连续性 ####
logfilename=$(date -d today +"%Y%m%d")
echo "===================================Message --=MongoDB 端口为" $port "的差异备份开始,开始时间为" $(date -d today +"%Y-%m-%-d %H:%M:%S") >> $bklogpath/$logfilename.log
ParamBakEndDate=$(date +%s)
echo "Message --本次备份时间参数中的结束时间为:" $(date -d @$ParamBakEndDate +"%Y-%m-%-d %H:%M:%S")
echo "Message --本次备份时间参数中的结束时间为:" $(date -d @$ParamBakEndDate +"%Y-%m-%-d %H:%M:%S") >> $bklogpath/$logfilename.log
echo "Message --本次备份时间参数中的结束时间戳:" $ParamBakEndDate >> $bklogpath/$logfilename.log
#DiffTime=$(expr 65 \* 60)
DiffTime=$(echo "60*60*24 + 10*60" |bc)
echo "Message --备份设置的间隔时间为:" $DiffTime >> $bklogpath/$logfilename.log
ParamBakStartDate=$(expr $ParamBakEndDate - $DiffTime)
echo "Message --本次备份时间参数中的开始时间为:" $(date -d @$ParamBakStartDate +"%Y-%m-%-d %H:%M:%S")
echo "Message --本次备份时间参数中的开始时间为:" $(date -d @$ParamBakStartDate +"%Y-%m-%-d %H:%M:%S") >> $bklogpath/$logfilename.log
echo "Message --本次备份时间参数中的开始时间戳:" $ParamBakStartDate >> $bklogpath/$logfilename.log
####检查 oplog 中最早的一笔数据,验证此次备份的时间是否在 oplog 记录的时间范围内
bkfilename=$(date -d today +"%Y%m%d%H%M%S")
command_line="${command_linebin} localhost:$port"
opmes=$(/bin/echo "db.printReplicationInfo()" | $command_line --quiet)
echo $opmes > opdoctime$port.tmplog
opbktmplogfile=opdoctime$port.tmplog
opstartmes=$(grep "oplog first event time" $opbktmplogfile | awk -F 'CST' '{print $1}' | awk -F 'oplog first event time: ' '{print $2}' | awk -F ' GMT' '{print $1}' )
echo "Message --oplog集合记录的开始时间为:"$opstartmes >> $bklogpath/$logfilename.log
oplogRecordFirst=$(date -d "$opstartmes" +%s)
echo "Message --oplog集合记录的开始时间为:" $oplogRecordFirst >> $bklogpath/$logfilename.log
## 比较备份参数的开始时间是否在 oplog 记录的时间范围内
if [ $oplogRecordFirst -le $ParamBakStartDate ];then
echo "Message --检查设置备份时间合理。备份参数的开始时间在oplog记录的时间范围内。" >> $bklogpath/$logfilename.log
else
echo "Fatal Error --检查设置的备份时间不合理合理。备份参数的开始时间不在oplog记录的时间范围内。请调整oplog size或调整备份频率。本次备份可以持续进行,但还原时数据完整性丢失。" >> $bklogpath/$logfilename.log
fi
##echo "{\"ts\":{\"\$gte\":{\"\$timestamp\": {\"t\": $ParamBakStartDate, \"i\": 1}},\"\$lte\":{\"\$timestamp\": {\"t\": $ParamBakEndDate, \"i\": 9999}}}}"
echo "{\"ts\":{\"\$gte\":{\"\$timestamp\": {\"t\": $ParamBakStartDate, \"i\": 1}},\"\$lte\":{\"\$timestamp\": {\"t\": $ParamBakEndDate, \"i\": 9999}}}}"
echo "{\"ts\":{\"\$gte\":{\"\$timestamp\": {\"t\": $ParamBakStartDate, \"i\": 1}},\"\$lte\":{\"\$timestamp\": {\"t\": $ParamBakEndDate, \"i\": 9999}}}}" > $bkdatapath/query.js
##que=${ echo "{ts:{\$gte:Timestamp($ParamBakStartDate,1),\$lte:Timestamp($ParamBakEndDate,9999)}}" }
##echo $que
# 执行 mongodump
mongodump -h localhost --port $port -d local -c oplog.rs --queryFile $bkdatapath/query.js -o $bkdatapath/mongodboplog$bkfilename >> $bklogpath/$logfilename.log 2>&1
#### 再次检查,防止导出 oplog 数据时间过长,而导致旧数据被更新,所以再次进行检查 ####
#### 因 oplog 的空间大小是固定的,而且数据是滚动记录的。####
#### 如果在导出期间有大量的写操作,则 oplog 中新的数据会覆盖掉旧的数据,就可能导致导出的数据不完整,无法保证增量文件间的时间连续性。 ####
#DiffTime=$(expr 61 \* 60)
DiffTime=$(echo "60*60*24 + 5*60" |bc)
ParamAfterBakRequestStartDate=$(expr $ParamBakEndDate - $DiffTime)
echo "Message --为保证备份的连续性,本次备份后,oplog中的开始时间需小于:" $ParamAfterBakRequestStartDate >> $bklogpath/$logfilename.log
opmes=$(/bin/echo "db.printReplicationInfo()" | $command_line --quiet)
echo $opmes > opdoctime$port.tmplog
opbktmplogfile=opdoctime$port.tmplog
opstartmes=$(grep "oplog first event time" $opbktmplogfile | awk -F 'CST' '{print $1}' | awk -F 'oplog first event time: ' '{print $2}' | awk -F ' GMT' '{print $1}' )
echo "Message --执行备份后,oplog集合记录的开始时间为:"$opstartmes >> $bklogpath/$logfilename.log
oplogRecordFirst=$(date -d "$opstartmes" +%s)
echo "Message --执行备份后,oplog集合记录的开始时间为[时间格式化]:" $oplogRecordFirst >> $bklogpath/$logfilename.log
##begin 比较备份参数的开始时间是否在oplog记录的时间范围内
if [ $oplogRecordFirst -le $ParamAfterBakRequestStartDate ];then
echo "Message --备份后,检查 oplo g集合中数据的开始时间,即集合中最早的一笔数据,时间不小于$ParamAfterBakRequestStartDate。这样可以保证每个增量备份含有最近一个小时的全部 op 操作,满足数据的持续完整性,逐个还原无丢失数据风险。" >> $bklogpath/$logfilename.log
else
echo "Fatal Error --备份后,检查 oplog 集合的涵盖的时间范围过小。设置的备份时间不合理,备份后的文件不能完全涵盖最近1天的数据。请调整oplog size或调整备份频率。本次备份可以持续进行,但还原时数据完整性丢失。" >> $bklogpath/$logfilename.log
fi
#### 检查备份文件是否生成 ####
if [ -d "$bkdatapath/mongodboplog$bkfilename" ];then
echo "Message --检查此次备份文件已经产生.文件信息为:" $bkdatapath/mongodboplog$bkfilename >> $bklogpath/$logfilename.log
mv $bkdatapath/mongodboplog$bkfilename/local/oplog.rs.bson $bkdatapath/mongodboplog$bkfilename/local/oplog.bson
cd $bkdatapath
tar -zcvf mongodboplog$bkfilename.tar.gz mongodboplog$bkfilename
echo "Message --备份打包压缩完成" $bkdatapath/mongodboplog$bkfilename.tar.gz >> $bklogpath/$logfilename.log >> $bklogpath/$logfilename.log
#推送数据库备份文件到备份服务器
echo "Message --传输数据库备份文件到备份服务器:" >> $bklogpath/$logfilename.log
rsync -avz mongodboplog$bkfilename.tar.gz $backupUser@$backupServerHost::$backupModule/ --password-file=/etc/rsync.pass >> $bklogpath/$logfilename.log
#删除此备份文件夹
rm -rf mongodboplog$bkfilename/
else
echo "Fatal Error --备份过程已执行,但是未检测到备份产生的文件,请检查!" >> $bklogpath/$logfilename.log
fi
#### 删除历史备份文件,保留7天,如需调整,请在持续设置
keepbaktime=$(date -d '-7 days' "+%Y%m%d%H")*
if [ -f $bkdatapath/mongodboplog$keepbaktime.tar.gz ];then
rm -f $bkdatapath/mongodboplog$keepbaktime.tar.gz
echo "Message -- $bkdatapath/mongodboplog$keepbaktime 删除完毕" >> $bklogpath/$logfilename.log
fi
echo "============================Message --MongoDB 端口为" $port "的差异备份结束,结束时间为:" $(date -d today +"%Y%m%d%H%M%S") >> $bklogpath/$logfilename.log
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
执行: sh /data/mongodb_back/script/mongodb_incr_back.sh
恢复增量备份数据
先登录mongo, 创建执行 mongorestore命令的专属权限用户root2,同上述mongo全量备份,如已创建则不用创建
执行命令:
先解压tar.gz
cd /data/mongodb_back/mongodb_backup_all/mongo-27017/
tar zxvf mongodboplog20230223180002.tar.gz
1
2
再执行恢复命令
mongorestore -h 127.0.0.1 -p port --username root2 --password 123456 --oplogReplay /data/mongodb_back/mongodb_oplog_back/mongo-27017/mongodboplog20230223180002/local
1
7.备份策略
全量备份7天一次,并删除7天前的备份数据,备份时间:04:00
增量备份1天一次,拉取oplog数据为增量数据备份,并删除7天前的备份数据, 备份时间: 03:30
服务器添加定时任务:
crontab -e
0 4 * * 1 sh /data/mongodb_back/script/mongodb_all_back.sh
30 3 * * * sh /data/mongodb_back/script/mongodb_incr_back.sh
1
2
3
4
crontab -l 查看定时任务
————————————————
版权声明:本文为CSDN博主「Hikes」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45967200/article/details/129255395