su 是用户切换的命令。```
su [user]
:切换到其他用户,但是不切换环境变量su - [user]
:则是完整的切换到新的用户环境
su 是用户切换的命令。```
su [user]
:切换到其他用户,但是不切换环境变量su - [user]
:则是完整的切换到新的用户环境参考:
Ubuntu 16.04 发布时,一个引人注目的新特性便是 apt
命令的引入。其实早在 2014 年,apt 命令就已经发布了第一个稳定版,只是直到 2016 年的 Ubuntu 16.04 系统发布时才开始引人关注。
随着 apt install package
命令的使用频率和普遍性逐步超过 apt-get install package
,越来越多的其它 Linux 发行版也开始遵循 Ubuntu 的脚步,开始鼓励用户使用 apt
而不是 apt-get
。
那么,apt-get
与 apt
命令之间到底有什么区别呢?如果它们有类似的命令结构,为什么还需要新的 apt
命令呢?是否 apt
真的比 apt-get
更好?普通用户应该使用新的 apt
命令还是坚持旧有习惯继续使用 apt-get
呢?
在开始对比 apt
与 apt-get
命令的区别之前,我们先来看看这两个命令的背景,以及它们要试图达到的目的。
Debian 作为 Ubuntu、Linux Mint 和 elementary OS 等 Linux 操作系统的母板,其具有强健的「包管理」系统,它的每个组件和应用程序都内置在系统中安装的软件包中。Debian 使用一套名为 Advanced Packaging Tool(APT)的工具来管理这种包系统,不过请不要把它与 apt
命令混淆,它们之间是其实不是同一个东西。
在基于 Debian 的 Linux 发行版中,有各种工具可以与 APT 进行交互,以方便用户安装、删除和管理的软件包。apt-get
便是其中一款广受欢迎的命令行工具,另外一款较为流行的是 Aptitude 这一命令行与 GUI 兼顾的小工具。
如果你已阅读过我们的 apt-get
命令指南,可能已经遇到过许多类似的命令,如 apt-cache
、apt-config
等。如你所见,这些命令都比较低级又包含众多功能,普通的 Linux 用户也许永远都不会使用到。换种说法来说,就是最常用的 Linux 包管理命令都被分散在了 apt-get
、apt-cache
和 apt-config
这三条命令当中。
apt
命令的引入就是为了解决命令过于分散的问题,它包括了 apt-get
命令出现以来使用最广泛的功能选项,以及 apt-cache
和 apt-config
命令中很少用到的功能。
在使用 apt
命令时,用户不必再由 apt-get
转到 apt-cache
或 apt-config
,而且 apt
更加结构化,并为用户提供了管理软件包所需的必要选项。
简单来说就是:
apt
=apt-get
、apt-cache
和apt-config
中最常用命令选项的集合。
通过 apt
命令,用户可以在同一地方集中得到所有必要的工具,apt
的主要目的是提供一种以「让终端用户满意」的方式来处理 Linux 软件包的有效方式。
apt
具有更精减但足够的命令选项,而且参数选项的组织方式更为有效。除此之外,它默认启用的几个特性对最终用户也非常有帮助。例如,可以在使用 apt
命令安装或删除程序时看到进度条。
apt
还会在更新存储库数据库时提示用户可升级的软件包个数。
如果你使用 apt
的其它命令选项,也可以实现与使用 apt-get
时相同的操作。
虽然 apt
与 apt-get
有一些类似的命令选项,但它并不能完全向下兼容 apt-get
命令。也就是说,可以用 apt
替换部分 apt-get
系列命令,但不是全部。
apt 命令 | 取代的命令 | 命令的功能 |
---|---|---|
apt install | apt-get install | 安装软件包 |
apt remove | apt-get remove | 移除软件包 |
apt purge | apt-get purge | 移除软件包及配置文件 |
apt update | apt-get update | 刷新存储库索引 |
apt upgrade | apt-get upgrade | 升级所有可升级的软件包 |
apt autoremove | apt-get autoremove | 自动删除不需要的包 |
apt full-upgrade | apt-get dist-upgrade | 在升级软件包时自动处理依赖关系 |
apt search | apt-cache search | 搜索应用程序 |
apt show | apt-cache show | 显示装细节 |
当然,apt
还有一些自己的命令:
新的apt命令 | 命令的功能 |
---|---|
apt list | 列出包含条件的包(已安装,可升级等) |
apt edit-sources | 编辑源列表 |
需要大家注意的是:apt
命令也还在不断发展,因此,你可能会在将来的版本中看到新的选项。
HBase 利用 Hadoop MapReduce 来处理 HBase 中的海量数据,实现高性能计算;利用 Zookeeper 作为协同服务,实现稳定服务和失败恢复;使用 HDFS 作为高可靠的底层存储,利用廉价集群提供海量数据存储能力。当然,HBase 也可以直接使用本地文件系统而不用 HDFS 作为底层数据存储方式,不过,为了提高数据可靠性和系统的健壮性,发挥 HBase 处理大数据量等功能,一般都使用 HDFS 作为 HBase 的底层数据存储方式。此外,为了方便在 HBase 上进行数据处理,Sqoop 为 HBase 提供了高效、便捷的 RDBMS 数据导人功能,Pig 和 Hive 为 HBase 提供了高层语言支持。HBase 是 BigTable 的开源实现。
HBase 是一个稀疏、多维度、排序的映射表,这张表的索引是行键、列族、列限定符和时间戳。每个值是一个未经解释的字符串,没有数据类型。用户在表中存储数据,每一行都有一个可排序的行键和任意多的列。表在水平方向由一个或者多个列族组成,同一个列族中可以包含任意多个列,同一个列族里面的数据存储在一起。列族支持动态扩展,可以很轻松地添加-个列族或列,无需预先定义列的数量以及类型,所有列均以字符串形式存储,用户需要自行进行数据类型转换。由于同-张表里面的每一行数据都可以有截然不同的列,因此对于整个映射表的每行数据而言,有些列的值就是空的,所以说 HBase 是稀疏的。
在 HBase 中执行更新操作时,并不会删除数据旧的版本,而是生成一个新的版本,旧有的版本仍然保留,HBase 可以对允许保留的版本的数量进行设置。客户端可以选择获取距离某个时间最近的版本,或者一次获取所有版本。如果在查询的时候不提供时间戳,那么会返回距离现在最近的那一个版本的数据,因为在存储的时候,数据会按照时间戳排序。
HBase 提供了两种数据版本回收方式:
HBase 采用表来组织数据,表由行和列组成,列划分为若千个列族。
每个 HBase 表都由若干行组成,每个行由行键(Row Key)来标识。访问表中的行只有 3 种方式:
行键可以是任意字符串(最大长度是 64 KB,实际应用中长度一般为 10~100 字节),在 HBase 内部,行键保存为字节数组。存储时,数据按照行键的字典序排序存储。在设计行键时,要充分考虑这个特性,将经常一起读取的行存储在一起。
一个 HBase 表被分组成许多“列族”的集合,它是基本的访问控制单元。列族需要在表创建时就定义好,数量不能太多(HBase 的一些缺陷使得列族数量只限于几十个),而且不要频繁修改。
存储在一个列族当中的所有数据,通常都属于同一种数据类型,这通常意味着具有更高的压缩率。表中的每个列都归属于某个列族,数据可以被存放到列族的某个列下面,但是在把数据存放到这个列族的某个列下面之前,必须首先创建这个列族。在创建完成一个列族以后,就可以使用同一个列族当中的列。列名都以列族作为前缀。例如,courses:history 和 courses:math 这两个列都属于 courses 这个列族。
在 HBase 中,访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,我们可以借助列族上的控制权限帮助实现特定的目的。比如,我们可以允许一些应用能够向表中添加新的数据,而另一些应用则只允许浏览数据。HBase 列族还可以被配置成支持不同类型的访问模式。比如,一个列族也可以被设置成放入内存当中,以消耗内存为代价,从而换取更好的响应性能。
列族里的数据通过列限定符(或列)来定位。列限定符不用事先定义,也不需要在不同行之间保持一致。列限定符没有数据类型,总被视为字节数组 byte[]。
在 HBase 表中,通过行、列族和列限定符确定一个“单元格”(Cell)。单元格中存储的数据没有数据类型,总被视为字节数组 bytel]。每个单元格中可以保存一个数据的多个版本,每个版本对应一个不同的时间戳。
每个单元格都保存着同一份数据的多个版本,这些版本采用时间戳进行索引。每次对一个单元格执行操作(新建、修改、删除)时,HBase 都会隐式地自动生成并存储一个时间戳。时间戳一般是 64 位整型,可以由用户自己赋值(自己生成唯一时间戳可以避免应用程序中出现数据版本冲突),也可以由 HBase 在数据写入时自动赋值。一个单元格的不同版本是根据时间戳降序的顺序进行存储的,这样,最新的版本可以被最先读取。
HBase 使用坐标来定位表中的数据,也就是说,每个值都是通过坐标来访问的。对于我们熟悉的关系数据库而言,数据定位可以理解为采用“二维坐标”,即根据行和列就可以确定表中一个具体的值。但是,HBase中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此可以视为一个“四维坐标”,即 [行键,列族,列限定符,时间戳]。
在 HBase 的概念视图中,一个表可以视为一个稀疏、多维的映射关系。
上图行键是一个反向 URL(即 com.cnn.www),之所以这样存放,是因为 HBase 是按照行键的字典序来排序存储数据的,采用反向 URL 的方式,可以让来自同一个网站的数据内容都保存在相邻的位置,在按照行键的值进行水平分区时,就可以尽量把来自同一个网站的数据划分到同一个分区(Region)中。
从概念视图层面,HBase 中的每个表是由许多行组成的,但是在物理存储层面,它是采用了基于列的存储方式,而不是像传统关系数据库那样采用基于行的存储方式,这也是 HBase 和传统关系数据库的重要区别。
行式数据库使用 NSM(N-ary Storage Model)存储模型,一个元组(或行)会被连续地存储在磁盘页中,也就是说,数据是一行一行被存储的,第一行写人磁盘页后,再继续写人第二行,依此类推。在从磁盘中读取数据时,需要从磁盘中顺序扫描每个元组的完整内容,然后从每个元组中筛选出查询所需要的属性。如果每个元组只有少量属性的值对于查询是有用的,那么 NSM 就会浪费许多磁盘空间和内存带宽。
列式数据库采用 DSM(Decomposition Storage Model)存储模型,它是在 1985 年提出来的,目的是最小化无用的 I/O。DSM 采用了不同于 NSM 的思路,对于采用 DSM 存储模型的关系数据库而言,DSM 会对关系进行垂直分解,并为每个属性分配一个子关系。因此,一个具有 n 个属性的关系会被分解成 n 个子关系,每个子关系单独存储,每个子关系只有当其相应的属性被请求时才会被访问。也就是说,DSM 是以关系数据库中的属性或列为单位进行存储,关系中多个元组的同一属性值(或同一列值)会被存储在一起,而一个元组中不同属性值则通常会被分别存放于不同的磁盘页中。
列式存储的优点是:
列式数据库主要用于数据挖掘、决策支持和地理信息系统等查询密集型系统中,因为一次查询就可以得出结果,而不必每次都要遍历所有的数据库。所以,列式数据库大多都是应用在人口统计调查、医疗分析等行业中,这种行业需要处理大量的数据统计分析,假如采用行式数据库,势必导致消耗的时间会无限放大。
DSM 存储模型的缺陷是:
HBase 的实现包括3个主要的功能组件:
因此,如果 Master 主服务器死机,那么整个系统都会无效。Master 会实时监测集群中的 Region 服务器,把特定的 Region 分配到可用的 Region 服务器上,并确保整个集群内部不同 Region 服务器之间的负载均衡,当某个 Region 服务器因出现故障而失效时,Master 会把该故障服务器上存储的 Region 重新分配给其他可用的 Region 服务器。除此以外,Master 还处理模式变化,如表和列族的创建。同客户端并不是直接从 Master 主服务器上读取数据,而是在获得 Region 的存储位置信息后,直接从 Region 服务器上读取数据。尤其需要指出的是,HBase 客户端并不依赖于 Master 而是借助于 Zookeeper 来获得 Region 的位置信息的,所以大多数客户端从来不和主服务器 Master 通信,这种设计方式使 Master 的负载很小。
在一个 HBase 中,存储了许多表。对于每个 HBase 表而言,表中的行是根据行键的值的字典序进行维护的,表中包含的行的数量可能非常庞大,无法存储在一台机器上,需要分布存储到多台机器上。因此,需要根据行键的值对表中的行进行分区,每个行区间构成一个分区,被称为”Region”,包含了位于某个值域区间内的所有数据,它是负载均衡和数据分发的基本单位,这些 Region 会被分发到不同的 Region 服务器上。
初始时,每个表只包含一个Region,随着数据的不断插入,Region 会持续增大,当一个 Region 中包含的行数量达到一个阈值时,就会被自动等分成两个新的 Region。随着表中行的数量继续增加,就会分裂出越来越多的 Region。
每个 Region 的默认大小是 100MB 到 200MB,是 HBase 中负载均衡和数据分发的基本单位。Master 主服务器会把不同的 Region 分配到不同的 Region 服务器上,但是同一个 Region 是不会被拆分到多个 Region 服务器上的。每个 Region 服务器负责管理-个 Region 集合,通常在每个 Region 服务器上会放置 10~1000 个 Region。
每个 Region 都有一个 RegionID 来标识它的唯一性,这样,一个 Region 标识符就可以表示成“表名 + 开始主键 + RegionID”。
有了 Region 标识符,就可以唯一标识每个 Region。为了定位每个 Region 所在的位置,就可以构建一张映射表,映射表的每个条目(或每行)包含两项内容,一个是 Region 标识符,另一个是 Region 服务器标识,这个条目就表示 Region 和 Region 服务器之间的对应关系,从而就可以知道某个 Region 被保存在哪个 Region 服务器中。这个映射表包含了关于 Region 的元数据(即 Region 和 Region 服务器之间的对应关系),因此也被称为“元数据表”,又名“.META.表”。
当一个 HBase 表中的 Region 数量非常庞大的时候,.META.表的条目就会非常多,一个服务器保存不下,也需要分区存储到不同的服务器上,因此.META.表也会被分裂成多个 Region,这时,为了定位这些 Region,就需要再构建一个新的映射表,记录所有元数据的具体位置,这个新的映射表就是“根数据表”,又名“-ROOT-表”。-ROOT-表是不能被分割的,永远只存在一个 Region 用于存放-ROOT-表,因此这个用来存放-ROOT-表的唯一一个 Region, 它的名字是在程序中被写死的,Master 主服务器永远知道它的位置。
综上所述,HBase 使用类似 B+树的三层结构来保存 Region 位置信息。
为了加快访问速度,.META.表的全部 Region 都会被保存在内存中。假设.META.表的每行(一个映射条目)在内存中大约占用 1KB,并且每个 Region 限制为 128MB,那么,上面的三层结构可以保存的用户数据表的 Region 数目的计算方法是:(-ROOT-表能够寻址的.META.表的 Region 个数) x (每个.META.表的 Region 可以寻址的用户数据表的 Region 个数)。一个-ROOT-表最多只能有一个 Region,也就是最多只能有 128MB,按照每行(一个映射条目)占用 1KB 内存计算,128MB 空间可以容纳 128MB/1KB=2^17 行,也就是说,一个-ROOT表可以寻址 2^17 个.META.表的 Region。同理,每个.META.表的 Region 可以寻址的用户数据表的 Region 个数是 128MB/1KB=2^17。最终,三层结构可以保存的 Region 数目是 (128MB/1KB) * (128MB/1KB) = 234 个 Region。可以看出,这种数量已经足够可以满足实际应用中的用户数据存储需求。
客户端访问用户数据之前,需要首先访问 Zookeeper,获取-ROOT-表的位置信息,然后访问-ROOT-表,获得.META.表的信息,接着访问.META.表,找到所需的 Region 具体位于哪个 Region 服务器,最后才会到该 Region 服务器读取数据。该过程需要多次网络操作,为了加速寻址过程,一般会在客户端做缓存,把查询过的位置信息缓存起来,这样以后访问相同的数据时,就可以直接从客户端缓存中获取 Region 的位置信息,而不需要每次都经历一个“三级寻址”过程。需要注意的是,随着 HBase 中表的不断更新,Region 的位置信息可能会发生变化,但是客户端缓存并不会自己检测 Region 位置信息是否失效,而是在需要访问数据时,从缓存中获取 Region 位置信息却发现不存在的时候,才会判断出缓存失效,这时,就需要再次经历上述的“三级寻址”过程,重新获取最新的 Region 位置信息去访问数据,并用最新的 Region 位置信息替换缓存中失效的信息。
当一个客户端从 Zookeeper 服务器上拿到-ROOT-表的地址以后,就可以通过“三级寻址”找到用户数据表所在的 Region 服务器,并直接访问该 Region 服务器获得数据,没有必要再连接主服务器 Master。因此,主服务器的负载相对就小了很多。
HBase 的系统架构包括客户端、Zookeeper 服务器、Master 主服务器、Region 服务器。需要说明的是,HBase 一般采用 HDFS 作为底层数据存储。
客户端包含访问 HBase 的接口,同时在缓存中维护着已经访问过的 Region 位置信息,用来加快后续数据访问过程。HBase 客户端使用 HBase 的 RPC 机制与 Master 和 Region 服务器进行通信。其中,对于管理类操作,客户端与 Master 进行 RPC;而对于数据读写类操作,客户端则会与 Region 服务器进行 RPC。
Zookeeper 服务器并非一台单一的机器,可能是由多台机器构成的集群来提供稳定可靠的协同服务。Zookeeper 能够很容易地实现集群管理的功能,如果有多台服务器组成一个服务器集群,那么必须有一个“总管”知道当前集群中每台机器的服务状态,一旦某台机器不能提供服务,集群中其他机器必须知道,从而做出调整重新分配服务策略。同样,当增加集群的服务能力时,就会增加一台或多台服务器,同样也必须让“总管”知道。
在 HBase 服务器集群中,包含了一个 Master 和多个 Region 服务器,Master 就是这个 HBase 集群的“总管”,它必须知道 Region 服务器的状态。Zookeeper 就可以轻松做到这一点,每个 Region 服务器都需要到 Zookeeper 中进行注册,Zookeeper 会实时监控每个 Region 服务器的状态并通知给 Master,这样,Master 就可以通过 Zookeeper 随时感知到各个 Region 服务器的工作状态。
Zookeeper 不仅能够帮助维护当前的集群中机器的服务状态,而且能够帮助选出一个“总管”,让这个总管来管理集群。HBase 中可以启动多个 Master,但是 Zookeeper 可以帮助选举出一个 Master 作为集群的总管,并保证在任何时刻总有唯一一个 Master 在运行,这就避免了 Master 的“单点失效”问题。
Zookeeper 中保存了-ROOT-表的地址和 Master 的地址,客户端可以通过访问 Zookeeper 获得-ROOT-表的地址,并最终通过“三级寻址”找到所需的数据。Zookeeper 中还存储了 HBase 的模式,包括有哪些表,每个表有哪些列族。
主服务器 Master 主要负责表和 Region 的管理工作。
客户端访问 HBase 上数据的过程并不需要 Master 的参与,客户端可以访问 Zookeeper 获取-ROOT-表的地址,并最终到达相应的 Region 服务器进行数据读写,Master 仅仅维护着表和 Region 的元数据信息,因此负载很低。
任何时刻,一个 Region 只能分配给一个 Region 服务器。Master 维护了当前可用的 Region 服务器列表,以及当前哪些 Region 分配给了哪些 Region 服务器,哪些 Region 还未被分配。当存在未被分配的 Region,并且有一个 Region 服务器上有可用空间时,Master 就给这个 Region 服务器发送一个请求,把该 Region 分配给它。Region 服务器接受请求并完成数据加载后,就开始负责管理该 Region 对象,并对外提供服务。
Region 服务器是 HBase 中最核心的模块,负责维护分配给自己的 Region,并响应用户的读写请求。HBase 一般采用 HDFS 作为底层存储文件系统,因此 Region 服务器需要向 HDFS 文件系统中读写数据。采用 HDFS 作为底层存储,可以为 HBase 提供可靠稳定的数据存储,HBase 自身并不具备数据复制和维护数据副本的功能,而HDFS可以为 HBase 提供这些支持。当然,HBase 也可以不采用 HDFS,而是使用其他任何支持 Hadoop 接口的文件系统作为底层存储,比如本地文件系统或云计算环境中的 AmazonS3(Simple Storage Service)。
Region 服务器是 HBase 中最核心的模块,Region 服务器内部管理了一系列 Region 对象和一个 HLog 文件,其中 HLog 是磁盘上面的记录文件,它记录着所有的更新操作。每个 Region 对象又是由多个 Store 组成的,每个 Store 对应了表中的一个列族的存储。每个 Store 又包含了一个 MemStore 和若千个 StoreFile。
当用户写入数据时,会被分配到相应的 Region 服务器去执行操作。用户数据首先被写人到 MemStore 和 HLog 中,当操作写入 HLog 之后,commit() 调用才会将其返回给客户端。
当用户读取数据时,Region 服务器会首先访问 MemStore 缓存,如果数据不在缓存中,才会到磁盘上面的 StoreFile 中去寻找。
MemStore 缓存的容量有限,系统会周期性地调用 Region.flushcache() 把 MemStore 缓存里面的内容写到磁盘的 StoreFile 文件中,清空缓存,并在 HLog 文件中写入一个标记,用来表示缓存中
的内容已经被写人 StoreFile 文件中。每次缓存刷新操作都会在磁盘上生成一个新的 StoreFile 文件,因此每个 Store 会包含多个 StoreFile 文件。
每个 Region 服务器都有一个自己的 HLog 文件,在启动的时候,每个 Region 服务器都会检查自己的 HLog 文件,确认最近一次执行缓存刷新操作之后是否发生新的写人操作。如果没有更新,说明所有数据已经被永久保存到磁盘的 StoreFile 文件中;如果发现更新,就先把这些更新写人 MemStore,然后再刷新缓存,写人到磁盘的 StoreFile 文件中。最后,删除旧的 HLog 文件,并开始为用户提供数据访问服务。
每次 MemStore 缓存的刷新操作都会在磁盘上生成一个新的 StoreFile 文件,这样,系统中的每个 Store 就会存在多个 StoreFile 文件。当需要访问某个 Store 中的某个值时,就必须查找所有这些 StoreFile 文件,非常耗费时间。因此,为了减少查找时间,系统一般会调用 Store.compact() 把多个 StoreFile 文件合并成一个大文件。由于合并操作比较耗费资源,因此只会在 StoreFile 文件的数量达到一个阈值的时候才会触发合并操作。
Region 服务器是 HBase 的核心模块,而 Store 则是 Region 服务器的核心。每个 Store 对应了表中的一个列族的存储。每个 Store 包含一个 MemStore 缓存和若于个 StoreFile 文件。MemStore 是排序的内存缓冲区,当用户写入数据时,系统首先把数据放人 MemStore 缓存,当 MemStore 缓存满时,就会刷新到磁盘中的一个 StoreFile 文件中。随着 StoreFile 文件数量的不断增加,当达到事先设定的数量时,就会触发文件合并操作,多个 StoreFile 文件会被合并成一个大的 StoreFile 文件。当多个 StoreFile 文件合并后,会逐步形成越来越大的 StoreFile 文件,当单个 StoreFile 文件大小超过一定阈值时,就会触发文件分裂操作。同时,当前的 1 个父 Region 会被分裂成 2 个子 Region, 父 Region 会下线,新分裂出的 2 个子 Region 会被 Master 分配到相应的 Region 服务器上。
在分布式环境下,必须要考虑到系统出错的情形,比如当 Region 服务器发生故障时,MemStore 缓存中的数据(还没有被写人文件)会全部丢失。因此,HBase 采用 HLog 来保证系统发生故障时能够恢复到正确的状态。
HBase 系统为每个 Region 服务器配置了一个 HLog 文件,它是一种预写式日志(Write Ahead Log),也就是说,用户更新数据必须首先被记人日志后才能写人 MemStore 缓存,并且直到 MemStore 缓存内容对应的日志已经被写入磁盘之后,该缓存内容才会被刷新写入磁盘。
Zookeeper 会实时监测每个 Region 服务器的状态,当某个 Region 服务器发生故障时,Zookeeper 会通知 Master。Master 首先会处理该故障 Region 服务器上面遗留的 HLog 文件,由于一个 Region 服务器上面可能会维护着多个 Region 对象,这些 Region 对象共用一个HLog文件,因此这个遗留的 HLog 文件中包含了来自多个 Region 对象的日志记录。系统会根据每条日志记录所属的 Region 对象对 HLog 数据进行拆分,分别放到相应 Region 对象的目录下,然后再将失效的 Region 重新分配到可用的 Region 服务器中,并把与该 Region 对象相关的 HLog 日志记录也发送给相应的 Region 服务器。Region 服务器领取到分配给自己的 Region 对象以及与之相关的 HLog 日志记录以后,会重新做一遍日志记录中的各种操作,把日志记录中的数据写人 MemStore 缓存,然后刷新到磁盘的 StoreFile 文件中,完成数据恢复。
需要特别指出的是,HBase 系统中,每个 Region 服务器只需要维护一个 HLog 文件,所有 Region 对象共用一个 HLog,而不是每个 Region 使用一个 HLog。在这种 Region 对象共用一个 HLog 的方式中,多个 Region 对象的更新操作所发生的日志修改,只需要不断把日志记录追加到单个日志文件中,而不需要同时打开、写入到多个日志文件中,因此可以减少磁盘寻址次数,提高对表的写操作性能。这种方式的缺点是,如果个 Region 服务器发生故障,为了恢复其上的 Region 对象,需要将 Region 服务器上的 HLog 按照其所属的 Region 对象进行拆分,然后分发到其他 Region 服务器上执行恢复操作。
HDFS(Hadoop Distributed File System,Hadoop 分布式文件系统)
在传统的文件系统中,为了提高磁盘读写效率,一般以数据块为单位,而不是以字节为单位。比如,机械式硬盘(磁盘的一种)包含了磁头和转动部件,在读取数据时有一个寻道的过程,通过转动盘片和移动磁头的位置,来找到数据在机械式硬盘中的存储位置,然后才能进行读写。在 I/O 开销中,机械式硬盘的寻址时间是最耗时的部分,一旦找到第条记录,剩下的顺序读取效率是非常高的。因此,以块为单位读写数据,可以把磁盘寻道时间分摊到大量数据中。
HDFS 也同样采用了块的概念,默认的一个块大小是 64 MB。
当客户端需要访问一个文件时,首先从名称节点获得组成这个文件的数据块的位置列表,然后根据位置列表获取实际存储各个数据块的数据节点的位置,最后数据节点根据数据块信息在本地 Linux 文件系统中找到对应的文件,并把数据返回给客户端。
名称节点(NameNode)负责管理分布式文件系统的命名空间(Namespace),保存了两个核心的数据结构,即 FsImage 和 EditLog。
名称节点记录了每个文件中各个块所在的数据节点的位置信息,但是并不持久化存储这些信息,而是在系统每次启动时扫描所有数据节点重构得到这些信息。
数据节点(DataNode)是分布式文件系统HDFS的工作节点,负责数据的存储和读取,会根据客户端或者名称节点的调度来进行数据的存储和检索,并且向名称节点定期发送自己所存储的块的列表。每个数据节点中的数据会被保存在各自节点的本地Linux文件系统中。
在名称节点运行期间,HDFS会不断发生更新操作,这些更新操作都是直接被写人到 EditLog 文件,因此 EditLog 文件也会逐渐变大。在名称节点运行期间,不断变大的 EditLog 文件通常对于系统性能不会产生显著影响,但是当名称节点重启时,需要将 FsImage 加载到内存中,然后逐条执行 EditLog 中的记录,使得 FsImage 保持最新。可想而知,如果 EditLog 很大,就会导致整个过程变得非常缓慢,使得名称节点在启动过程中长期处于“安全模式”,无法正常对外提供写操作,影响了用户的使用。
为了有效解决 EditLog 逐渐变大带来的问题,HDFS 在设计中采用了第二名称节点(Secondary NameNode)。第二名称节点是 HDFS 架构的一个重要组成部分,具有两个方面的功能:
具体如下:
HDFS 的命名空间包含目录、文件和块。命名空间管理是指命名空间支持对 HDFS 中的目录、文件和块做类似文件系统的创建、修改、删除等基本操作。在当前的 HDFS 体系结构中,在整个 HDFS 集群中只有一个命名空间,并且只有唯一一个名称节点,该节点负责对这个命名空间进行管理。
HDFS 使用的是传统的分级文件体系,因此用户可以像使用普通文件系统样,创建、删除目录和文件,在目录间转移文件、重命名文件等。但是,HDFS 还没有实现磁盘配额和文件访问权限等功能,也不支持文件的硬连接和软连接(快捷方式)。
HDFS 是一个部署在集群上的分布式文件系统,因此很多数据需要通过网络进行传输。所有的 HDFS 通信协议都是构建在 TCP/IP 协议基础之上的。客户端通过一个可配置的端口向名称节点主动发起 TCP 连,并使用客户端协议与名称节点进行交互。名称节点和数据节点之间则使用数据节点协议进行交互。客户端与数据节点的交互是通过 RPC(Remote Procedure Call)来实现的。在设计上,名称节点不会主动发起 RPC,而是响应来自客户端和数据节点的 RPC 请求。
HDFS只设置唯一一个名称节点,这样做虽然大大简化了系统设计,但也带来了一些明显的局限性,具体如下。
HDFS 采用了多副本方式对数据进行冗余存储,通常一个数据块的多个副本会被分布到不同的数据节点上。
多副本方式具有以下 3 个优点:
HDFS 默认的冗余复制因子是 3,每一个文件块会被同时保存到 3 个地方,其中,有两份副本放在同一个机架的不同机器上面,第三个副本放在不同机架的机器上面,这样既可以保证机架发生异常时的数据恢复,也可以提高数据读写性能(同机架内带宽高)。一般而言,HDFS 副本的放置策略如下:
HDFS 提供了一个 API 可以确定一个数据节点所属的机架 ID,客户端也可以调用 API 获取自已所属的机架 ID。当客户端读取数据时,从名称节点获得数据块不同副本的存放位置列表,列表中包含了副本所在的数据节点,可以调用 API 来确定客户端和这些数据节点所属的机架 ID。当发现某个数据块副本对应的机架 ID 和客户端对应的机架 ID 相同时,就优先选择该副本读取数据,如果没有发现,就随机选择-个副本读取数据。
HDFS 的数据复制采用了流水线复制的策略,大大提高了数据复制过程的效率。当客户端要往 HDFS 中写入一个文件时,这个文件会首先被写入本地,并被切分成若千个块,每个块的大小是由 HDFS 的设定值来决定的。每个块都向 HDFS 集群中的名称节点发起写请求,名称节点会根据系统中各个数据节点的使用情况,选择一个数据节点列表返回给客户端,然后客户端就把数据首先写入列表中的第一个数据节点,同时把列表传给第一个数据节点,当第一个数据节点接收到 4 KB 数据的时候,写入本地,并且向列表中的第二个数据节点发起连接请求,把自己已经接收到的 4 KB 数据和列表传给第二个数据节点,当第二个数据节点接收到 4 KB 数据的时候,写人本地,并且向列表中的第三个数据节点发起连接请求,依次类推,列表中的多个数据节点形成一条数据复制的流水线。最后,当文件写完的时候,数据复制也同时完成。
Hadoop 采用两种机制来确保名称节点的安全:
每个数据节点会定期向名称节点发送“心跳”信息,向名称节点报告自己的状态。当数据节点发生故障,或者网络发生断网时,名称节点就无法收到来自一些数据节点的“心跳”信息,这时这些数据节点就会被标记为“宕机”,节点上面的所有数据都会被标记为“不可读”,名称节点不会再给它们发送任何 IO 请求。
这时,有可能出现一种情形,即由于些数据节点的不可用,会导致一些数据块的副本数量小于冗余因子。名称节点会定期检查这种情况,一旦发现某个数据块的副本数量小于冗余因子,就会启动数据冗余复制,为它生成新的副本。HDFS 与其他分布式文件系统的最大区别就是可以调整冗余数据的位置。
网络传输和磁盘错误等因素都会造成数据错误。客户端在读取到数据后,会采用 md5 和 sha1 对数据块进行校验,以确定读取到正确的数据。在文件被创建时,客户端就会对每一个文件块进行信息摘录,并把这些信息写人同一个路径的隐藏文件里面。当客户端读取文件的时候,会先读取该信息文件,然后利用该信息文件对每个读取的数据块进行校验,如果校验出错,客户端就会请求到另外一个数据节点读取该文件块,并且向名称节点报告这个文件块有错误,名称节点会定期检查并且重新复制这个块。
转自:https://blog.csdn.net/zzti_erlie/article/details/103665903
众所周知,索引类似于字典的目录,可以提高查询的效率。
索引从物理上可以分为:聚集索引,非聚集索引
从逻辑上可以分为:普通索引,唯一索引,主键索引,联合索引,全文索引
在列上进行运算或使用函数会使索引失效,从而进行全表扫描。如下面例子在 publish_time,id 列上分别加上索引,publish_time 为 datetime 类型,id 为 int 类型
1 | -- 全表扫描 |
1 | -- 走索引 |
1 | -- 全表扫描 |
1 | -- 走索引 |
假设 id 为 varchar 类型
1 | -- 全表扫描 |
1 | -- 走索引 |
为什么呢?
1 | select * from article where id = 100 |
上一条规则说过,不要在索引列上使用函数,隐式类型转换在索引字段上做了函数操作,因此会全表扫描
那么如果 id 是 int,执行下面这个语句是否会导致全表扫描呢?
1 | select * from article where id = '100' |
答案是会用到索引,我们来分析一下为什么会用到索引
我们先来做一个实验,看一下数据库中字符串和数字做比较的时候,是怎么转换的?
这里有个简单的方法执行select “10” > 9即可
1 | mysql> select "10" > 9; |
结果为 1 表明当字符串和数字进行比较的时候,是把字符串转成数字
1 | mysql> select "a" = 0; |
从实验结果中可以看到,当字符串不含有数字时,会转成 0,否则转成字符串中第一段连续的数字
我们接着来分析上面的例子,为什么一会会用到索引,一会不会用到
1 | -- id列上有索引,id为varchar,不会走索引 |
1 | -- 全表扫描 |
%李,%李%都会导致全表扫描,非前导模糊查询可以使用索引
1 | -- 走索引 |
mysql 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d 的顺序可以任意调整
当不需要考虑排序和分组时,将区分度最高的列放在前面通常是很好的。这时候索引的作用只是用于优化 WHERE 条件的查找
如果在 a b 列上建立联合索引,该如何建立,才能使查询效率最高
1 | select count(distinct a) / count(*), count(distinct b) / count(*), count(*) from table |
执行如下语句,假设 3 个输出依次为 0.0001,0.373,16049,可以看到 b 列的选择性最高,因此将其作为联合索引的第一列,即建立(b, a)的联合索引
查询时=可以乱序
如果建立了联合索引(a, b)。例如下面的2个写法是等价的,因为MySQL会将查询的顺序优化成和联合索引的顺序一致
1 | select * from table where a = '1' and b = '1' |
1 | select * from table where b = '1' and a = '1' |
优化查询,避免出现 filesort
1 | select * from table where a = ? and b = ? order by c |
最左前缀原则不仅用在查询中,还能用在排序中。MySQL 中,有两种方式生成有序结果集:
通过有序索引顺序扫描直接返回有序数据
Filesort排序,对返回的数据进行排序
因为索引的结构是 B+树,索引中的数据是按照一定顺序进行排列的,所以在排序查询中如果能利用索引,就能避免额外的排序操作。EXPLAIN 分析查询时,Extra 显示为 Using index。
所有不是通过索引直接返回排序结果的操作都是 Filesort 排序,也就是说进行了额外的排序操作。EXPLAIN 分析查询时,Extra 显示为 Using filesort,当出现 Using filesort 时对性能损耗较大,所以要尽量避免 Using filesort
对于如下sql
1 | select * from table where a = ? and b = ? order by c |
可以建立联合索引(a, b, c)
如果索引中有范围查找,那么索引有序性无法利用,如
1 | select * from table where a > 10 order by b |
索引(a,b)无法排序。
放几个例子
1 | -- 使用了a列 |
1 | select * from article where id = 1 |
1 | select * from article where id in (1 , 2) |
新版MySQL的or可以命中索引
1 | select * from article where id = 1 or id = 2 |
效率从高到低为union,in,or。in 和 union 的效率差别可以忽略不计,建议使用 in
负向条件有:!=、<>、not in、not exists、not like 等
1 | -- 全表扫描 |
知道 id 的所有取值范围,可以改为类似如下形式
1 | -- 走索引 |
众所周知,表数据是放在一个聚集索引上的,而建立的索引为非聚集索引,非聚集索引的叶子节点存放索引键值,以及该索引键指向的主键。一般查找的过程是从非聚集索引上找到数据的主键,然后根据该主键到聚集索引上查找记录,这个过程称为回表,不清楚的看推荐阅读。
如有下面这个sql
1 | select uid, login_time from user where username = ? and passwd = ? |
可以建立(username, passwd, login_time)的联合索引,由于 login_time 的值可以直接从索引中拿到,不用再回表查询,提高了查询效率
更新会变更 B+ 树,更新频繁的字段建立索引会大大降低数据库性能。
“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似。
一般区分度在80%以上的时候就可以建立索引,区分度可以使用 count(distinct(列名))/count(*)
来计算
当查询确定只有一条记录时,可以加 liimit 1,让 MySQL 停止游标移动,提高查询效率
1 | select uid from user where username = ? and passwd = ? |
可改为
1 | select uid from user where username = ? and passwd = ? limit 1 |
用邮箱登录是一个常见的问题,如果对email整个字段建立索引,会让索引变得大且慢
1 | select username from user where email='xxx'; |
这时我们可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率,但这样也会降低索引的区分度。索引的区分度是指,不重复的索引值和数据表的记录总数的比值。索引的区分度越高则查询效率越高,因为区分度高的索引可以让MySQL在查找时过滤掉更多的行。
因此我们选择足够长的前缀保证较高的区分度,同时又不能太长(以便节约空间)
可以进行如下实验
1 | select count(distinct left(email, 5)) / count(*) as col5, |
假设输出依次为0.0305,0.0309,0.0310
查询显示当前缀长度达到7的时候,再增加前缀长度,区分度提升的幅度已经很小了,因此创建email(7)的前缀索引即可
需要注意的一点是,前缀索引不能使用覆盖索引
只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL值,那么这一列对于此复合索引就是无效的。
因此,在数据库设计时,除非有一个很特别的原因使用 NULL 值,不然尽量不要让字段的默认值为 NULL。
FIFO(First in First out),先进先出。其实在操作系统的设计理念中很多地方都利用到了先进先出的思想,比如作业调度(先来先服务),为什么这个原则在很多地方都会用到呢?因为这个原则简单、且符合人们的惯性思维,具备公平性,并且实现起来简单,直接使用数据结构中的队列即可实现。
在 FIFO Cache 设计中,核心原则就是:如果一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。在 FIFO Cache 中应该支持以下操作;
get(key)
:如果 Cache 中存在该 key,则返回对应的 value 值,否则,返回 -1;set(key,value)
:如果 Cache 中存在该 key,则重置 value 值;如果不存在该 key,则将该 key 插入到到 Cache 中,若 Cache 已满,则淘汰最早进入 Cache 的数据。举个例子:假如 Cache 大小为 3,访问数据序列为 set(1,1),set(2,2),set(3,3),set(4,4),get(2),set(5,5)
则 Cache 中的数据变化为:
1 | (1,1) set(1,1) |
那么利用什么数据结构来实现呢?
下面提供一种实现思路:
利用一个双向链表保存数据,当来了新的数据之后便添加到链表末尾,如果 Cache 存满数据,则把链表头部数据删除,然后把新的数据添加到链表末尾。在访问数据的时候,如果在 Cache 中存在该数据的话,则返回对应的 value 值;否则返回 -1。如果想提高访问效率,可以利用 hashmap 来保存每个 key 在链表中对应的位置。
LFU(Least Frequently Used)最近最少使用算法。它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。
注意 LFU 和 LRU 算法的不同之处,LRU 的淘汰规则是基于访问时间,而 LFU 是基于访问次数的。举个简单的例子:
假设缓存大小为 3,数据访问序列为 set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4)
,
则在 set(4,4)
时对于 LFU 算法应该淘汰 (3,3)
,而 LRU 应该淘汰 (1,1)
。
那么 LFU Cache 应该支持的操作为:
get(key)
:如果 Cache 中存在该 key,则返回对应的 value 值,否则,返回 -1;set(key,value)
:如果 Cache 中存在该 key,则重置 value 值;如果不存在该 key,则将该 key 插入到到 Cache 中,若 Cache 已满,则淘汰最少访问的数据。为了能够淘汰最少使用的数据,因此 LFU 算法最简单的一种设计思路就是 利用一个数组存储数据项,用 hashmap 存储每个数据项在数组中对应的位置,然后为每个数据项设计一个访问频次,当数据项被命中时,访问频次自增,在淘汰的时候淘汰访问频次最少的数据。这样一来的话,在插入数据和访问数据的时候都能达到 $O(1)$ 的时间复杂度,在淘汰数据的时候,通过选择算法得到应该淘汰的数据项在数组中的索引,并将该索引位置的内容替换为新来的数据内容即可,这样的话,淘汰数据的操作时间复杂度为 $O(n)$。
另外还有一种实现思路就是利用小顶堆+hashmap,小顶堆插入、删除操作都能达到 $O(logn)$ 时间复杂度,因此效率相比第一种实现方法更加高效。
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
过程如下:
转自:https://www.jianshu.com/p/8845ddca3b23
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC 在 MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个point的概念
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本
undo log主要分为两种:
purge
- 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
- 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:
一、 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
二、 现在来了一个事务1对该记录的name做出了修改,改为Tom
三、 又来了个事务2修改person表的同一个记录,将age修改为30岁
从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
那么这个判断条件是什么呢?
如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不完全哈,但能看出大致逻辑),该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较
在展示之前,我先简化一下Read View,我们可以把Read View简单的理解成有三个全局属性
我们在了解了隐式字段,undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了
整体的流程是怎么样的呢?我们可以模拟一下
也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
当前读和快照读在RR级别下的区别:
表1:
表2:
而在表2这里的顺序中,事务B在事务A提交后的快照读和当前读都是实时的新数据400,这是为什么呢?
所以我们知道事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
我们这里测试的是更新,同时删除和更新也是一样的,如果事务B的快照读是在事务A操作之后进行的,事务B的快照读也是能读取到最新的数据的
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
转自:https://zhuanlan.zhihu.com/p/27700617
平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构;
平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下规则:
平衡树的层级结构:因为平衡二叉树查询性能和树的层级(h高度)成反比,h值越小查询越快、为了保证树的结构左右两端数据大致平衡降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树,使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找;
总结平衡二叉树特点:
注意:之前有看到有很多文章把B树和B-tree理解成了两种不同类别的树,其实这两个是同一种树;
B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),数据库索引技术里大量使用者B树和B+树的数据结构,让我们来看看他有什么特点;
最后我们用一个图和一个实际的例子来理解B树(这里为了理解方便我就直接用实际字母的大小来排列C>B>A)
如上图我要从上图中找到E字母,查找流程如下
定义一个5阶树(平衡5路查找树;),现在我们要把3、8、31、11、23、29、50、28 这些数字构建出一个5阶树出来;
遵循规则:
先插入 3、8、31、11
再插入23、29
再插入50、28
规则:
B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;
B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别
(百度百科算法结构示意图)
(维基百科算法结构示意图)
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
B*树是B+树的变种,相对于B+树他们的不同之处如下:
在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树额分解次数变得更少;
相同思想和策略
从平衡数据结构-树-二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度;
不同的方式的磁盘空间利用
不同点是他们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的;
转自:https://zhuanlan.zhihu.com/p/85837641
不能出现有重复的ID标识,这是基本要求。
确保生成ID对于用户或业务是递增的。
确保任何时候都能生成正确的ID。
在高并发的环境下依然表现良好。
Java自带的生成一串唯一随机36位字符串(32个字符串+4个“-”)的算法。它可以保证唯一性,且据说够用N亿年,但是其业务可读性差,无法有序递增。
今天的主角雪花算法,它是Twitter开源的由64位整数组成分布式ID,性能较高,并且在单机上递增。 具体参考:https://github.com/twitter-archive/snowflake
UidGenerator是百度开源的分布式ID生成器,其基于雪花算法实现。 具体参考:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
Leaf是美团开源的分布式ID生成器,能保证全局唯一,趋势递增,但需要依赖关系数据库、Zookeeper等中间件。 具体参考:https://tech.meituan.com/MT_Leaf.html
SnowFlake是Twitter公司采用的一种算法,目的是在分布式系统中产生全局唯一且趋势递增的ID。
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢:: 同一毫秒的ID数量 = 1024 X 4096 = 4194304
雪花算法的实现主要依赖于数据中心ID和数据节点ID这两个参数,具体实现如下。
1 | public class SnowflakeIdWorker { |
1 | # coding: utf-8 |
转自:https://blog.csdn.net/ycb1689/article/details/89331634
互联网快速发展的今天,分布式应用系统已经见怪不怪,在分布式系统中,我们需要各种各样的ID,既然是ID那么必然是要保证全局唯一,除此之外,不同当业务还需要不同的特性,比如像并发巨大的业务要求ID生成效率高,吞吐大;比如某些银行类业务,需要按每日日期制定交易流水号;又比如我们希望用户的ID是随机的,无序的,纯数字的,且位数长度是小于10位的。等等,不同的业务场景需要的ID特性各不一样,于是,衍生了各种ID生成器,但大多数利用数据库控制ID的生成,性能受数据库并发能力限制,那么有没有一款不需要依赖任何中间件(如数据库,分布式缓存服务等)的ID生成器呢?本着取之于开源,用之于开源的原则,今天,特此介绍Twitter开源的一款分布式自增ID算法snowflake,并附上算法原理推导和演算过程!
snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,性能每秒生成300w+。
总体来说,在工作节点达到1024顶配的场景下,SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?这是一个简单的乘法:
同一毫秒的ID数量 = 1024 X 4096 = 4194304
400多万个ID,这个数字在绝大多数并发场景下都是够用的。
snowflake 算法中,第三个部分是工作机器ID,可以结合上一节的命名方法,并通过Zookeeper管理workId,免去手动频繁修改集群节点,免去配置机器ID的麻烦。
上面只是一个将64bit划分的标准,当然也不一定这么做,可以根据不同业务的具体场景来划分,比如下面给出一个业务场景:
这个时候我们根据上面的场景可以再次合理的划分62bit,QPS几年之内会发展到百万,那么每毫秒就是千级的请求,目前10台机器那么每台机器承担百级的请求,为了保证扩展,后面的循环位可以限制到1024,也就是2^10,那么循环位10位就足够了。
机器三地部署我们可以用3bit总共8来表示机房位置,当前的机器10台,为了保证扩展到百台那么可以用7bit 128来表示,时间位依然是41bit,那么还剩下64-10-3-7-41-1 = 2bit,还剩下2bit可以用来进行扩展。
适用场景:当我们需要无序不能被猜测的ID,并且需要一定高性能,且需要long型,那么就可以使用我们雪花算法。比如常见的订单ID,用雪花算法别人就发猜测你每天的订单量是多少。
上面定义了雪花算法的实现,在nextId中是我们生成雪花算法的关键。
因为机器的原因会发生时间回拨,我们的雪花算法是强依赖我们的时间的,如果时间发生回拨,有可能会生成重复的ID,在我们上面的nextId中我们用当前时间和上一次的时间进行判断,如果当前时间小于上一次的时间那么肯定是发生了回拨,普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况:
通过上面的几种策略可以比较的防护我们的时钟回拨,防止出现回拨之后大量的异常出现。下面是修改之后的代码,这里修改了时钟回拨的逻辑:
最终代码如下:
1 | import org.apache.commons.lang3.RandomUtils; |
在 Python 中,整数 0 代表假,整数 1 代表真。不过,除此之外,Python 也把任何空数据结构视为假,把任何非空数据结构视为真。更一般地,真和假的概念是 Python 中每个对象的固有属性:每个对象非真即假。