前面我们已经讲到很多的树,比如普通二叉树,二叉堆,二叉查找树,平衡二叉树等。那现在有一个问题,这么多的树都是用来干什么的?其实啊,任何事物都有着发展的必然性,都是为了解决问题。而随着问题规模和深度的不断加深,对应的解决方案也随之发展。这些树大多都是为了解决查找效率,或者是保证查找结果的有序性。实际业务场景中,无非读和写(删除和更新算是写的一种)。针对写操作,更看重的是稳定性,正确写是第一位的,写的速度是其次,因为生产数据的来源流量也肯定不是很大,多花时间来写是ok的。但是读就是持续追求效率的方向,历史数据不断积累,获取数据太慢就会严重影响到系统性能,到用户端就是实实在在的体验感。所以,针对读效率的问题,发展出了各式各样的数据结构,以期提高查找效率。从二叉查找树到平衡二叉树就是这样演化的过程。我们知道平衡二叉树的查找时间复杂度已经是O(logN)了,其实已经是最快的了。但是历史数据肯定是要存储在磁盘中的,防止丢失,磁盘IO也是影响查找效率的重要因素。随着大数据的到来,数据规模不断扩大,达到百万,千万,亿乃至十亿级别以上,查找时磁盘IO成为了主要的性能瓶颈。因为平衡二叉树在磁盘上的存储是不连续的,每次磁盘IO读取的元素数量有限,导致磁盘IO次数增大,总体上严重影响查找性能。原有的解决方案已经不太适用这种场景。所以要谈到我们今天要讲的B-树,来提高大规模数据场景下的查找效率。
B-树,即为B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种树。而事实上是,B-tree就是指的B树。
B树的出现是为了弥合不同的存储级别之间的访问速度上的巨大差异,实现高效的 I/O。平衡二叉树的查找效率是非常高的,并可以通过降低树的深度来提高查找的效率。但是当数据量非常大,树的存储的元素数量是有限的,这样会导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。另外数据量过大会导致内存空间不够容纳平衡二叉树所有结点的情况。B树是解决这个问题的很好的结构
首先,B树不要和二叉树混淆,在计算机科学中,B树是一种自平衡树数据结构
,它维护有序数据
并允许以对数时间进行搜索,顺序访问,插入和删除
。B树是二叉搜索树的一般化,因为节点可以有两个以上的子节点。与其他自平衡二进制搜索树不同,B树非常适合读取和写入相对较大的数据块(如光盘)的存储系统。它通常用于数据库和文件系统。
数据库的索引是存储在磁盘上的(比如mysql的InnoDB引擎使用B+树,作为索引树结构,B+树是B-树的变形),当数据量比较大的时候,索引的大小可能有几个G甚至更多。当我们利用索引查询的时候,能把整个索引全部加载到内存嘛?显然不可能。能做的只有逐一加载每一个磁盘页,这里的磁盘页对应这索引树的节点
,也是mysql的page页
补充说明:索引树在磁盘上的分布是不连续的,通过节点指针进行连接。磁盘IO有一个预读的机制(下面会专门讲到),我们在查找节点是进行磁盘IO每次读取都是一个磁盘页,该节点的信息包含在该磁盘页中,所以上面说一个磁盘页对应一个索引树的节点。磁盘页的大小跟操作系统有关,一般是4K或8K,所以一个节点包含的信息大小不能超过一个磁盘页的大小。
我们知道平衡二叉树的查找时间复杂度是O(logN)的,那为什么不利用二叉查找树作为索引结构呢?用平衡二叉树时,假设树的高度为4,要查找的值为10,流程如下:
平衡二叉树:
第一次查找(第一磁盘IO,读取第一份磁盘页,下同):
…(省略中间过程)
第四次磁盘IO:
从结果上看,最坏情况下,磁盘IO的次数等于索引树的高度。
这时要提高查找效率,方向就是建少磁盘IO次数,将原本"瘦高"的树结构变为"矮胖"的树结构(即减少树的高度,增加每个节点存储的元素数量,同时节点大小不能超过磁盘页的大小),这就是B树的特征之一。
上面我们一直在讲磁盘IO,这里我们做一个简单介绍。
计算机存储设备一般分为两种:
内存储器(main memory)和外存储器(external memory)。
内存储器为内存,内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据(在不通电情况下数据会消失)。
外存储器即为磁盘读取,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms(磁盘是圈,所以除以2);传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。下图是计算机硬件延迟的对比图,供大家参考:
考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page),也即磁盘页
。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
事实1 : 不同容量的存储器,访问速度差异悬殊。
事实2 : 从磁盘中读 1 B,与读写 1KB 的时间成本几乎一样
从以上数据中可以总结出一个道理,索引查询的数据主要受限于硬盘的I/O速度,查询I/O次数越少,速度越快,所以B树的结构才应需求而生;B树的每个节点的元素可以视为一次I/O读取,树的高度表示最多的I/O次数,在相同数量的总元素个数下,每个节点的元素个数越多,高度越低,查询所需的I/O次数越少;假设,一次硬盘一次I/O数据为8K,索引用int(4字节)类型数据建立,理论上一个节点最多可以为2000个元素,2000*2000*2000=8000000000,80亿条的数据只需3次I/O(理论值),可想而知,B树做为索引的查询效率有多高
;
另外也可以看出同样的总元素个数,查询效率和树的高度密切相关
B树是一种平衡的多分树,通常我们说m阶的B树,它或者是空树,或者必须满足如下条件:
注:
math.ceil(x)是返回数字的上入整数。例如 math.ceil(1.2)返回2。
补充说明:
把握一点:
B树相较于二叉查找树,要降低树的高度及增加节点元素的个数,所以每个父节点可能有2个以上的子节点,每个节点的元素个数也不再限制为1
什么是B树的阶 ?
B树中所有节点的子节点数目的最大值,用m表示,假如最大值为10,则为10阶B树
一棵含有N个总关键字数的m阶的B树的最大高度是多少?
log(m/2)(N+1)/2 + 1 ,log以(m/2)为低,(N+1)/2的对数再加1