HBase原理 – snapshot 快照

目录

snapshot(快照)基础原理

snapshot能实现什么功能?

hbase snapshot用法大全

hbase snapshot分布式架构-两阶段提交

snapshot核心实现

clone_snapshot如何实现呢?

其他需要注意的

参考文献


更多信息可参考《Hbase原理与实战》第十一章 备份与恢复

snapshot(快照)基础原理

snapshot是很多存储系统和数据库系统都支持的功能。一个snapshot是一个全部文件系统、或者某个目录在某一时刻的镜像。实现数据文件镜像最简单粗暴的方式是加锁拷贝(之所以需要加锁,是因为镜像得到的数据必须是某一时刻完全一致的数据),拷贝的这段时间不允许对原数据进行任何形式的更新删除,仅提供只读操作,拷贝完成之后再释放锁。这种方式涉及数据的实际拷贝,数据量大的情况下必然会花费大量时间,长时间的加锁拷贝必然导致客户端长时间不能更新删除,这是生产线上不能容忍的。

 

snapshot机制并不会拷贝数据,可以理解为它是原数据的一份指针。在HBase这种LSM类型系统结构下是比较容易理解的,我们知道HBase数据文件一旦落到磁盘之后就不再允许更新删除等原地修改操作,如果想更新删除的话可以追加写入新文件(HBase中根本没有更新接口,删除命令也是追加写入)。这种机制下实现某个表的snapshot只需要给当前表的所有文件分别新建一个引用(指针),其他新写入的数据重新创建一个新文件写入即可。如下图所示:


1

 

snapshot流程主要涉及3个步骤:

1. 加一把全局锁,此时不允许任何的数据写入更新以及删除

2. 将Memstore中的缓存数据flush到文件中(可选)

3. 为所有HFile文件分别新建引用指针,这些指针元数据就是snapshot

 

扩展思考:LSM类系统确实比较容易理解,那其他非LSM系统原地更新的存储系统如何实现snapshot呢?

 

snapshot能实现什么功能?

snapshot是HBase非常核心的一个功能,使用snapshot的不同用法可以实现很多功能,比如:

  1. 全量/增量备份:任何数据库都需要有备份的功能来实现数据的高可靠性,snapshot可以非常方便的实现表的在线备份功能,并且对在线业务请求影响非常小。使用备份数据,用户可以在异常发生的情况下快速回滚到指定快照点。增量备份会在全量备份的基础上使用binlog进行周期性的增量备份。
  • 使用场景一:通常情况下,对重要的业务数据,建议至少每天执行一次snapshot来保存数据的快照记录,并且定期清理过期快照,这样如果业务发生重要错误需要回滚的话是可以回滚到之前的一个快照点的。
  • 使用场景二:如果要对集群做重大的升级的话,建议升级前对重要的表执行一次snapshot,一旦升级有任何异常可以快速回滚到升级前。

       2. 数据迁移:可以使用ExportSnapshot功能将快照导出到另一个集群,实现数据的迁移

  • 使用场景一:机房在线迁移,通常情况是数据在A机房,因为A机房机位不够或者机架不够需要将整个集群迁移到另一个容量更大的B集群,而且在迁移过程中不能停服。基本迁移思路是先使用snapshot在B集群恢复出一个全量数据,再使用replication技术增量复制A集群的更新数据,等待两个集群数据一致之后将客户端请求重定向到B机房。具体步骤可以参考:https://www.cloudera.com/documentation/enterprise/5-5-x/topics/cdh_bdr_hbase_replication.html#topic_20_11_7
  • 使用场景二:使用snapshot将表数据导出到HDFS,再使用Hive\Spark等进行离线OLAP分析,比如审计报表、月度报表等

hbase snapshot用法大全

snapshot最常用的命令有snapshot、restore_snapshot、clone_snapshot以及ExportSnapshot这个工具,具体使用方法如下:

  • 为表’sourceTable’打一个快照’snapshotName’,快照并不涉及数据移动,可以在线完成。
hbase> snapshot 'sourceTable', ‘snapshotName'
  • 恢复指定快照,恢复过程会替代原有数据,将表还原到快照点,快照点之后的所有更新将会丢失。需要注意的是原表需要先disable掉,才能执行restore_snapshot操作。
hbase> restore_snapshot ‘snapshotName'
  • 根据快照恢复出一个新表,恢复过程不涉及数据移动,可以在秒级完成。很好奇是怎么做的吧,且听下文分解。
hbase> clone_snapshot 'snapshotName', ‘tableName'
  • 使用ExportSnapshot命令可以将A集群的快照数据迁移到B集群,ExportSnapshot是HDFS层面的操作,会使用MR进行数据的并行迁移,因此需要在开启MR的机器上进行迁移。HMaster和HRegionServer并不参与这个过程,因此不会带来额外的内存开销以及GC开销。唯一的影响是DN在拷贝数据的时候需要额外的带宽以及IO负载,ExportSnapshot也针对这个问题设置了参数-bandwidth来限制带宽的使用。
hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \-snapshot MySnapshot -copy-from hdfs://srv2:8082/hbase \-copy-to hdfs://srv1:50070/hbase -mappers 16 -bandwidth  1024\

 

 

hbase snapshot分布式架构-两阶段提交

hbase为指定表执行snapshot操作,实际上真正执行snapshot的是对应表的所有region。这些region因为分布在多个RegionServer上,所以需要一种机制来保证所有参与执行snapshot的region要么全部完成,要么都没有开始做,不能出现中间状态,比如某些region完成了,某些region未完成。

 

HBase使用两阶段提交协议(2PC)来保证snapshot的分布式原子性。2PC一般由一个协调者和多个参与者组成,整个事务提交分为两个阶段:prepare阶段和commit阶段。其中prepare阶段协调者会向所有参与者发送prepare命令,所有参与者开始获取相应资源(比如锁资源)并执行prepare操作确认可以执行成功,通常核心工作都是在prepare操作中完成的。并返回给协调者prepared应答。协调者接收到所有参与者返回的prepared应答之后(表明所有参与者都已经准备好提交),在本地持久化commit状态,进入commit阶段,协调者会向所有参与者发送commit命令,参与者接收到commit命令之后会执行commit操作并释放资源,通常commit操作都非常简单。

 

接下来就看看hbase是如何使用2PC协议来构建snapshot架构的,基本步骤如下:

 

1. prepare阶段:HMaster在zookeeper创建一个’/acquired-snapshotname’节点,并在此节点上写入snapshot相关信息(snapshot表信息)。所有regionserver监测到这个节点之后,根据/acquired-snapshotname节点携带的snapshot表信息查看当前regionserver上是否存在目标表,如果不存在,就忽略该命令。如果存在,遍历目标表中的所有region,分别针对每个region执行snapshot操作,注意此处snapshot操作的结果并没有写入最终文件夹,而是写入临时文件夹。regionserver执行完成之后会在/acquired-snapshotname节点下新建一个子节点/acquired-snapshotname/nodex,表示nodex节点完成了该regionserver上所有相关region的snapshot准备工作。

 

2. commit阶段:一旦所有regionserver都完成了snapshot的prepared工作,即都在/acquired-snapshotname节点下新建了对应子节点,hmaster就认为snapshot的准备工作完全完成。master会新建一个新的节点/reached-snapshotname,表示发送一个commit命令给参与的regionserver。所有regionserver监测到/reached-snapshotname节点之后,执行snapshot commit操作,commit操作非常简单,只需要将prepare阶段生成的结果从临时文件夹移动到最终文件夹即可。执行完成之后在/reached-snapshotname节点下新建子节点/reached-snapshotname/nodex,表示节点nodex完成snapshot工作。

 

3. abort阶段:如果在一定时间内/acquired-snapshotname节点个数没有满足条件(还有regionserver的准备工作没有完成),hmaster认为snapshot的准备工作超时。hmaster会新建另一种新的节点/abort-snapshotname,所有regionserver监听到这个命令之后会清理snapshot在临时文件夹中生成的结果。

 

可以看到,在这个系统中HMaster充当了协调者的角色,RegionServer充当了参与者的角色。HMaster和RegionServer之间的通信通过Zookeeper来完成,同时,事务状态也是记录在Zookeeper上的节点上。HMaster高可用情况下主HMaster宕机了,从HMaster切成主后根据Zookeeper上的状态可以决定事务十分继续提交或者abort。

 

snapshot核心实现

上节从架构层面介绍了snapshot如何在分布式体系中完成原子性操作。那每个region是如何真正实现snapshot呢?hmaster又是如何汇总所有region snapshot结果?

 

region如何实现snapshot?

在基本原理一节我们提到过snapshot不会真正拷贝数据,而是使用指针引用的方式创建一系列元数据。那元数据具体是什么样的元数据呢?实际上snapshot的整个流程基本如下:

2

 

 

分别对应debug日志中如下片段:

 

 

snapshot.FlushSnapshotSubprocedure: Flush Snapshotting region yixin:yunxin,user1359,1502949275629.77f4ac61c4db0be9075669726f3b72e6. started...
snapshot.SnapshotManifest: Storing 'yixin:yunxin,user1359,1502949275629.77f4ac61c4db0be9075669726f3b72e6.' region-info for snapshot.
snapshot.SnapshotManifest: Creating references for hfiles
snapshot.SnapshotManifest: Adding snapshot references for [] hfiles

注意:region生成的snapshot文件是临时文件,生成目录在/hbase/.hbase-snapshot/.tmp下,一般因为snapshot过程特别快,所以很难看到单个region生成的snapshot文件。

 

 

hmaster如何汇总所有region snapshot的结果?

hmaster会在所有region完成snapshot之后执行一个汇总操作(consolidate),将所有region snapshot manifest汇总成一个单独manifest,汇总后的snapshot文件是可以在HDFS目录下看到的,路径为:/hbase/.hbase-snapshot/snapshotname/data.manifest。注意,snapshot目录下有3个文件,如下图所示:

3

 

其中.snapshotinfo为snapshot基本信息,包含待snapshot的表名称以及snapshot名;data.manifest为snapshot执行后生成的元数据信息,即snapshot结果信息。可以使用hadoop dfs -cat /hbase/.hbase-snapshot/snapshotname/data.manifest 查看:

4

 

 

clone_snapshot如何实现呢?

前文提到snapshot可以用来搞很多大事情,比如restore_snapshot、clone_snapshot以及export snapshot等等,这节就来看看clone_snapshot这个功能具体是如何实现的。直接进入正题,整个步骤可以概括为如下:

  1. 预检查:确认目标表没有进行snapshot操作以及restore操作,否则直接返回错误
  2. 在tmp文件夹下新建表目录并在表目录下新建.tabledesc文件,在该文件中写入表schema信息
  3. 新建region目录:这个步骤是clone_snapshot和create table最大的不同,新建的region目录是依据snapshot manifest中信息确定的,region中有哪些列族?列族中有哪些HFile文件?都来源于此。

此处有一个很有意思的事情是clone_snapshot克隆表的过程中并不涉及数据的移动,那不禁要问克隆出的表中文件是什么文件?与原表中数据文件之间的对应关系如何建立?这个问题的解决和split过程中reference文件的解决思路基本一致,不过在clone_snapshot中并不称作reference文件,而叫做linkfile,和reference文件不一样的是linkfile文件没有任何内容,只是在文件名上做了文章,比如原文件名是abc,生成的linkfile就为:table=region-abc,通过这种方式就可以很容易定位到原表中原始文件的具体路径:xxx/table/region/hfile,因此就可以不需要移动数据了。

5

上图中LinkFile文件名为music=5e54d8620eae123761e5290e618d556b-f928e045bb1e41ecbef6fc28ec2d5712,根据定义我们知道music为原始文件的表名,5e54d8620eae123761e5290e618d556b为引用文件所在的region,f928e045bb1e41ecbef6fc28ec2d5712为引用文件,如下图所示:

6

我们可以依据规则可以直接根据LinkFile的文件名定位到引用文件所在位置:***/music/5e54d8620eae123761e5290e618d556b/cf/f928e045bb1e41ecbef6fc28ec2d5712,如下图所示:

7
       4. 将表目录从tmp文件夹下移动到hbase root location

       5. 修改meta表,将克隆表的region信息添加到meta表中,注意克隆表的region名称和原数据表的region名称并不相同(region名称与table名称相关,table名不同,region名称就肯定不会相同)

       6. 将这些region通过round-robin方式立刻均匀分配到整个集群中,并在zk上将克隆表的状态设置为enabled,正式对外提供服务

 

其他需要注意的

不知道大家有没有关注另一个问题,按照上文的说法我们知道snapshot实际上是一系列原始表的元数据,主要包括表schema信息、原始表所有region的region info信息,region包含的列族信息以及region下所有的hfile文件名以及文件大小等。那如果原始表发生了compaction导致hfile文件名发生了变化或者region发生了分裂,甚至删除了原始表,之前所做的snapshot是否就失效了?

 

从功能实现的角度来讲肯定不会让用户任何时间点所作的snapshot失效,那如何避免上述所列的各种情况下snapshot失效呢?HBase的实现也比较简单,在原始表发生compact的操作前会将原始表复制到archive目录下再执行compact(对于表删除操作,正常情况也会将删除表数据移动到archive目录下),这样snapshot对应的元数据就不会失去意义,只不过原始数据不再存在于数据目录下,而是移动到了archive目录下。

 

大家可以做一下这样一个实验看看:

 

1. 使用snapshot给一张表做快照,比如snapshot ’test’,’test_snapshot’
2. 查看archive目录,确认不存在目录:/hbase-root-dir/archive/data/default/test
3. 对表test执行major_compact操作:major_compact ’test’
4. 再次查看archive目录,就会发现test原始表移动到了该目录,/hbase-root-dir/archive/data/default/test就会存在

同理,如果对原始表执行delete操作,比如delete ’test’,也会在archive目录下找到该目录。和普通表删除的情况不同的是,普通表一旦删除,刚开始是可以在archive中看到删除表的数据文件,但是等待一段时间后archive中的数据就会被彻底删除,再也无法找回。这是因为master上会启动一个定期清理archive中垃圾文件的线程(HFileCleaner),定期会对这些被删除的垃圾文件进行清理。但是snapshot原始表被删除之后进入archive,并不可以被定期清理掉,上文说过clone出来的新表并没有clone真正的文件,而是生成的指向原始文件的连接,这类文件称之为LinkFile,很显然,只要LinkFile还指向这些原始文件,它们就不可以被删除。好了,这里有两个问题:

1. 什么时候LinkFile会变成真实的数据文件?

如果看过笔者上篇文章《HBase原理 – 所有Region切分的细节都在这里了》的同学,肯定看着这个问题有种似曾相识的赶脚。不错,HBase中一个region分裂成两个子region后,子region的文件也是引用文件,这些引用文件是在执行compact的时候才真正将父region中的文件迁移到自己的文件目录下。LinkFile也一样,在clone出的新表执行compact的时候才将合并后的文件写到新目录并将相关的LinkFile删除,理论上也是借着compact顺便做了这件事。

 

2. 系统在删除archive中原始表文件的时候怎么知道这些文件还被一些LinkFile引用着?

HBase Split后系统要删除父region的数据文件,是首先要确认两个子region已经没有引用文件指向它了,系统怎么确认这点的呢?上节我们分析过,meta表中会存储父region对应的两个子region,再扫描两个子region的所有文件确认是否还有引用文件,如果已经没有引用文件了,就可以放心地将父region的数据文件删掉了,当然,如果还有引用文件存在就只能作罢。

 

那删除clone后的原始表文件,是不是也是一样的套路?答案并不是,HBase用了另一种方式来根据原始表文件找到引用文件,这就是back-reference机制。HBase系统在archive目录下新建了一种新的back-reference文件,来帮助原始表文件找到引用文件。来看看back-reference文件是一种什么样的文件,它是如何根据原始文件定位到LinkFile的:

(1)原始文件:/hbase/data/table-x/region-x/cf/file-x
(2)clone生成的LinkFile:/hbase/data/table-cloned/region-y/cf/{table-x}-{region-x}-{file-x},因此可以很容易根据LinkFile定位到原始文件
(3)back-reference文件:/hbase/.archive/data/table-x/region-x/cf/.links-file-x/{region-y}.{table-cloned},可以看到,back-reference文件路径中包含所有原始文件和LinkFile的信息,因此可以有效的根据原始文件/table-x/region-x/cf/file-x定位到LinkFile:/table-cloned/region-y/cf/{table-x}-{region-x}-{file-x}

 

到这里,有兴趣的童鞋可以将这块知识点串起来做个简单的小实验:

(1)使用snapshot给一张表做快照,比如snapshot ’table-x’,’table-x-snapshot’

(2)使用clone_snapshot克隆出一张新表,比如clone_snapshot ’table-x-snapshot’,’table-x-cloned’。并查看新表test_clone的HDFS文件目录,确认会存在LinkFile

8

 

(3)删除原表table-x(删表之前先确认archive下没有原表文件),查看确认原表文件进入archive,并在archive中存在back-reference文件。注意瞅瞅back-reference文件格式哈。

9

10

 

 

 

(4)对表’table-x-clone’执行major_compact,命令为major_compact ’test_clone’。执行命令前确认table-x-clone文件目录下LinkFile存在。

(5)major_compact执行完成之后查看table-x-clone的HDFS文件目录,确认所有LinkFile已经不再存在,全部变成了真实数据文件。

11

 

 

参考文献

Introduction to Apache HBase Snapshots:http://blog.cloudera.com/blog/2013/03/introduction-to-apache-hbase-snapshots/

Introduction to Apache HBase Snapshots, Part 2: Deeper Dive:http://blog.cloudera.com/blog/2013/06/introduction-to-apache-hbase-snapshots-part-2-deeper-dive/

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

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

相关文章

linux如何自动化部署脚本实现免密登录并访问资源

任务把weijie主机jdk文件安装到weijie1中。 首先再各台主机中安装必要的命令: expect、wget、httpd、ssh 执行命令 如:expect提示命令不存在,则分别安装命令 yum install expect yum install wget yum install httpd yum install ssh 开…

时序数据库技术体系 – InfluxDB TSM存储引擎之数据读取

任何一个数据库系统内核关注的重点无非:数据在内存中如何存储、在文件中如何存储、索引结构如何存储、数据写入流程以及数据读取流程。关于InfluxDB存储内核,笔者在之前的文章中已经比较全面的介绍了数据的文件存储格式、倒排索引存储实现以及数据写入流…

java多线程之生产者和消费者问题

线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信,协调完成工作. 经典的生产者和消费者案例(Producer/Consumer):分析案例:1):生产者和消费者应该操作共享的资源(实现方式来做).2):使用一个或多个线程来表示生产者(Producer).3):使用一个或多个…

时序数据库技术体系 – InfluxDB TSM存储引擎之数据写入

之前两篇文章笔者分别从TSM File文件存储格式、倒排索引文件存储格式这两个方面对InfluxDB最基础、最底层也最核心的存储模块进行了介绍,接下来笔者会再用两篇文章在存储文件的基础上分别介绍InfluxDB是如何处理用户的写入(删除)请求和读取请…

zookeeper结构和命令详解

1.1. zookeeper特性1、Zookeeper:一个leader,多个follower组成的集群 2、全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的 3、分布式读写,更新请求转发&#xf…

时序数据库技术体系 – InfluxDB 多维查询之倒排索引

在时序数据库概述一文中,笔者提到时序数据库的基础技术栈主要包括高吞吐写入实现、数据分级存储|TTL、数据高压缩率、多维度查询能力以及高效聚合能力等,上文《时序数据库技术体系 – InfluxDB存储引擎TSM》基于InfluxDB存储引擎TSM介绍了时序…

OSG框架分析

本文参考<<osg最长一帧>>, <<OpenSceneGraph三维渲染引擎编程指南>>, <<OpenSceneGraph三维渲染引擎设计与实践>> 整理而来, 感谢大牛们的精彩著作. 相比Ogre来说, Ogre代码很规范, 只是入门资料较少,如果能在学习之前能总体上对架构有个…

在Eclipse中如何操作zookpeer

导入jar包 jar包下载链接 代码解析 package com.itcast.zookpeer.zk;import java.io.IOException; import java.util.List;import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apac…

Linux 系统进程守护工具 cesi + superviosr

一、安装 Supervisor pip install supervisor 使用 echo_supervisord_conf 命令生成默认配置文件 echo_supervisord_conf > /etc/supervisord.conf 配置文件说明 位置&#xff1a;etc/supervisord.conf内容&#xff1a;# 指定了socket file的位置 [unix_http_server] f…

Docker 服务器安装(一)

使用官方安装脚本自动安装 安装命令如下&#xff1a; curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun 也可以使用国内 daocloud 一键安装命令&#xff1a; curl -sSL https://get.daocloud.io/docker | sh 设置docker 加速器 sudo curl -sSL https…

Docker 入门使用 (二)

配置国内的源 > /etc/docker/daemon.json{"registry-mirrors" : ["https://mirror.ccs.tencentyun.com","http://registry.docker-cn.com","http://docker.mirrors.ustc.edu.cn","http://hub-mirror.c.163.com"],"…

ElasticSearch sql 插件安装

PS&#xff1a;6.3 开始 ElasticSearch 自身已经支持SQL查询。 github地址&#xff1a;https://github.com/NLPchina/elasticsearch-sql 一、在线安装 直接执行 ./bin/elasticsearch-plugin install https://github.com/NLPchina/elasticsearch-sql/releases/download/6.3.…

zookpeer实现对服务器动态上下线的监听

服务器动态上下线程序的工作机制 服务器代码&#xff1a; 补充&#xff1a;volatile关键字&#xff1a;java中一切都是对象&#xff0c;当多个线程操作同一个对象时候&#xff0c;该对象会放在堆内存中&#xff0c;而多个线程相当于在多个栈中&#xff0c;当A线程想要去除对…

Result window is too large, from + size must be less than or equal to: [10000] but was [12390]. See

ES 查询报错 Caused by: java.lang.IllegalArgumentException: Result window is too large, from size must be less than or equal to: [10000] but was [12390]. See the scroll api for a more efficient way to request large data sets. This limit can be set by chan…

java中泛型学习总结

为什么需要使用泛型: 1):存储任意类型的数据在集合中 ,但是取出来都是Object类型的,此时就得强转.List list new ArrayList();list.add(1); //Interger类型Object ele list.get(0); //现在需要调用Interger类中的方法I nterger num (Interger) ele;System.out.println(num);…

别说“我已经很努力了”

转自&#xff1a;http://blog.csdn.net/foruok/article/details/40247543 我们程序员的努力与挣扎有时非常尴尬&#xff0c;如果没有结果&#xff0c;都是徒然&#xff0c;都是说不得说不得…… 我自己做项目经理时&#xff0c;干的项目也经常延期……非常惭愧。而延期其实对研…

Java集合框架-概述

Java集合框架的由来: 其实在Java2(jdk1.2)之前&#xff0c;Java是没有完整的集合框架的。它只有一些简单的可以自扩展的容器类&#xff0c;比如Vector&#xff0c;Stack&#xff0c;Hashtable等。 为什么存在容器类: 容器类(集合类)可以存储多个数据,既然数组可以存储多个数据…

MySQL Binlog增量同步工具go-mysql-transfer实现详解

go-mysql-transfer产品手册:https://www.kancloud.cn/wj596/go-mysql-transfer/2111996 一、 概述 工作需要研究了下阿里开源的MySQL Binlog增量订阅消费组件canal&#xff0c;其功能强大、运行稳定&#xff0c;但是有些方面不是太符合需求&#xff0c;主要有如下三点&#x…

std::thread详解

转自&#xff1a;http://www.cnblogs.com/haippy/p/3236136.html 上一篇博客《C11 并发指南一(C11 多线程初探)》中只是提到了 std::thread 的基本用法&#xff0c;并给出了一个最简单的例子&#xff0c;本文将稍微详细地介绍 std::thread 的用法。 std::thread 在 <thread&…

std::mutex详解

Mutex 又称互斥量&#xff0c;C 11中与 Mutex 相关的类&#xff08;包括锁类型&#xff09;和函数都声明在 <mutex> 头文件中&#xff0c;所以如果你需要使用 std::mutex&#xff0c;就必须包含 <mutex> 头文件。 <mutex> 头文件介绍 Mutex 系列类(四种) st…