从数据库底层来看,数据库到目前为止,有这几种数据结构
1.b树,b+树,(mysql,PostgreSQL)
2.lsm树(LevelDB,RocksDB,TIDB,CockroachDB)
3.基于B+树和LSM树改良的树,比如Fractal树(TokuDB)
下面一一介绍每种树的典型应用
B树,B+树
传统的关系型数据将数据以B树的形式存储在磁盘上,它们也会在RAM上使用B树维护这些数据的索引,来保证更快的访问速度。插入的行存储在B树的叶子节点上,所有的中间节点用来存储用于导航查询语句的原数据。因此,当有数以百万计的数据被插入到数据库中时,索引和数据存储会变得十分大。因此,为了快速的访问,需要从磁盘中加载所有数据到内存,但是RAM一般没有这么大的空间来存储所有的数据。因此,数据库必须从磁盘中读取部分数据。这种加载数据的场景如下图所示:
图片引自https://segmentfault.com/a/1190000015892186
B树和B+树区别在于B树的内部节点有指向关键字具体信息的指针,而b+树没有,因此b+树相对b树,有如下优点
磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
InnoDB
mysql的InnoDB是大家工作中最常用的数据库存储引擎,B+树,聚簇索引,行级锁。聚簇索引数据的物理存放与索引都是有序的,它把所有的卫星数据都存储在叶节点中,内部节点只存放关键字和孩子指针。
图片引自https://blog.csdn.net/u010842515/article/details/68929687
InnoDB的所有辅助索引都引用主键作为data域。InnoDB 不会压缩索引,所以,如果主键定义的比较大,其他索引也将很大。如果想在表上定义很多索引,则争取尽量把主键定义得小一些。辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
图片引自https://blog.csdn.net/u010842515/article/details/68929687
页是InnoDB存储引擎的最小管理单位,每页大小默认是16KB。以64位操作系统,long型主键为例,一个索引页可以装下大概16k/(8+8)=1k个关键字,因为每一级下级索引页也可以装下1k个关键字,而一级索引指向的二级索引有1k个,因此二级索引就有1k*1k=100万个关键字。如果数据库一行数据在100字节,那么每一个数据页最多可以装下16*1024/100=160条数据(实际情况是mysql不会把每页的数据都装满,否则再插入一条数据,当前页就装不下了,就得在页间挪动数据了),假如这颗树只有两级索引,那么每相邻的二级索引指向的叶子节点就有160行数据。那么这样一颗二级索引的树可以指向100万*160=1.6亿数据。
以64位操作系统,36位uuid做主键为例,一个索引页可以装下大概16*1024/(36+8)= 372个下级索引,二级索引就有372*372=13万个关键字,如果一行数据在1k字节,那么每个数据页可以装下16行数据,那么这样一个二级索引树可以指向13万*16=208万行数据。
综上innodb的数据和索引都是顺序排列的。innodb数据库设计优化的重点在主键索引的长度控制,其次在于控制每行的数据量。设计并发比较高的系统的时候,可以采用冷热分离的数据库设计方法,对于热数据,主键采用自增的方式,优先保证数据的插入速度。对于冷数据来说,主键可以采用用户经常批量查询的字段做主键(比如用户id+热数据id),可以充分利用聚簇索引的数据连续的特点。尽量减少读取磁盘的次数。
MyISAM
mysql数据库的MyISAM表也是基于B+Tree的,它的叶子节点上的data,并不是数据本身,而是数据存放的地址。主索引和辅助索引没啥区别,只是主索引中的key一定得是唯一的。这里的索引都是非聚簇索引。他的数据的物理地址是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,于是开始不停的寻道不停的旋转。不过,如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。MyISAM引擎由于更新的时候采用表锁的方式,目前多应用在只读数据的查询方面。
图片引自https://blog.csdn.net/u010842515/article/details/68929687
LSM树
传统数据库的写入是有瓶颈的,为了迎合以写为主的系统的需要,数据库需要能够拥有快速插入数据的能力。因此LSM树应运而生。
LSM的原理是有写入请求的时候, 为了保护内存中的数据,在磁盘上先记录logfile(当内存中的数据flush到磁盘上时,就可以抛弃相应的Logfile),然后写入到内存中(内存没有寻道速度的问题,随机写的性能得到大幅提升),在内存中构建一颗有序小树,随着小树越来越大,内存的小树会flush到磁盘上。随着小树越来越多,读的性能会越来越差,因此需要在适当的时候,对磁盘中的小树进行merge,多棵小树变成一颗大树。当读时,优先看内存中有没有匹配的key,如果有则返回,没有则遍历所有的树(RocksDB,Cassandra增加了Bloom Filter来判断请求的key是否可能在树中),但在每颗小树内部数据是有序的。
图片引自https://www.cnblogs.com/yanghuahui/p/3483754.html
LevelDB,RocksDB
LevelDB 是由 Google 开发的单机版 key-value存储系统,是基于 LSM(Log-Structured-Merge Tree) 的典型实现,RocksDB是Facebook基于LevelDB优化的一种嵌入式单机版Key-value存储系统,该数据库能够充分利用闪存的性能,大大提升应用服务器的速度,在其上基于一致性协议可以构建复杂的系统,比如TIDB,CockroachDB。
Fractal树
Fractal树是一种写优化的磁盘索引数据结构,集合了LSM写入速度快的优点和b+树读取快的优点。它索引维护了类似b+树的结构,利用索引节点的MessageBuffer缓存更新操作,充分利用数据局部性原理, 将随机写转换为顺序写,这样极大的提高了随机写的效率。从理论复杂度和测试性能两个角度上看, Fractal树的Insert/Delete/Update操作性能优于B+树。 但是读操作性能低于B+树。Fractal树支持在做DDL操作的同时(例如添加索引),用户依然可以执行写入操作, 这个特点是Fractal树树形结构天然支持的。Tokutek研发团队的iiBench测试结果显示: TokuDB(采用Fractal树)的insert操作(随机写)的性能比InnoDB快很多,而Select操作(随机读)的性能低于InnoDB的性能,但是差距较小
在Fractal树中,进行的添加列,删除列,插入,更新等任何操作都会被当做操作消息存储在非叶节点上。由于操作只是被简单地存储在缓存或者任何次级索引缓存(secondary index buffer)中,所有的操作都会被迅速执行结束。当某一个节点的缓存满了之后,这些操作消息会依次从根节点,经过非叶节点,向叶节点进行传递。叶节点仍然存储着真实数据。当进行读时,读操作会考虑查询路径节点上的所有操作消息来获取真实的数据状态。但是由于tokudb会尽力将所有非叶节点缓存在内存中,所以这一过程也很快。只是分形树的范围查询基本等价于进行N次单条数据查询操作,因此效率比b+树低很多
在线更新表结构实现也是基于消息来实现的, 但和Insert/Delete/Update操作不同的是, 前者的消息下推方式是广播式下推(父节点的一条消息,应用到所有的儿子节点), 后者的消息下推方式单播式下推(父节点的一条消息,应用到对应键值区间的儿子节点), 实现类似于Insert操作。
图片引自https://segmentfault.com/a/1190000015892186
NewSQL
单机数据库满足不了互联网线上数据规模,google Spanner/F1论文的发表,给大家指明了一个新的方向。出现了NewSQL这个概念,NewSQL是指这样一类新式的关系型数据库管理系统,针对OLTP(读-写)工作负载,追求提供和NoSQL系统相同的扩展性能,且仍然保持ACID和SQL等特性,冲破CAP的枷锁,在三者之间完美平衡,开源领域以TIDB,CockroachDB为代表
CAP理论:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
图片引自http://www.hollischuang.com/archives/666
TIDB
TiDB 是 PingCAP 公司受 Google Spanner / F1 论文启发而设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。一个 TiDB 集群拥有几个 TiDB 服务、几个 TiKV 服务和一组 Placement Deiver(PD)(通常 3-5 个节点)。TiDB 服务是无状态 SQL 层,TiKV 服务是键值对存储层,PD 则是管理组件,从顶层视角负责存储元数据以及负载均衡,TiDB 可以进行在线 DDL
图片引自https://www.oschina.net/translate/how-to-do-performance-tuning-on-tidb
假设我们使用如下 SQL 来插入一条数据到表 t
INSERT INTO t(id, name, address) values(1, “Jack”, “Sunnyvale”);
执行流程如下图
图片引自https://www.oschina.net/translate/how-to-do-performance-tuning-on-tidb
TiDB 只存储键值对,那么要如何实现诸如数据库、表和索引等高层概念呢?在 TiDB 中,每个表都有一个关联的全局唯一编号,被称为 “table-id”。特定表中的所有数据(包括记录和索引)的键都是以 8 字节的 table-id 开头的。对于每行表数据,它的 key 是一个 64 位整数,称为 Handle ID。如果一张表存在 int 类型的主键,会把主键的值当作表数据的 Handle ID,否则由系统自动生成 Handle ID。表数据的 value 由这一行的所有数据编码而成。在读取表数据的时候,可以按照 Handle ID 递增的顺序返回。
TiDB 的索引数据和表数据一样,也存放在 TiKV 中。每个索引都有一个名为 “index-id” 的表范围的唯一编号。它的 key 是由索引列编码的有序 bytes,value 是这一行索引数据对应的 Handle ID,通过 Handle ID 我们可以读取这一行的非索引列。
在 TiDB 中,Region 表示一个连续的、左闭右开的键值范围 [start_key,end_key)。每个 Region 有多个副本,并且每个副本称为一个 peer 。每个 Region 也归属于单独的 Raft 组,以确保所有 peer 之间的数据一致性。同一表的临近记录很可能位于同一 Region 中。当集群第一次初始化时,只存在一个 Region 。当 Region 达到特定大小(当前默认值为96MB)时, Region 将动态分割为两个邻近的 Region ,并自动将数据分布到系统中以提供水平扩展。
3.0版本后,tidb增加了Range分区和Hash分区的方式,一下将调度的灵活性提高了,对分片的索引采用hash分区,这样一方面让数据库避免了热点的问题,另一方面对于数据的定位更快了,时间复杂度o(1)。对于大表来说设计好数据的分片就可以了,取数据的时候取到的数据就在一个分片上,避免了数据不在一台机器上的大范围网络查询。
TiDB支持数据导出功能,通过一个叫作 Pump 的小程序,汇总写入到 Kafka 集群。在下游有一个叫 Drainer 的组件来消费 Kafka 的数据,按照事务的顺序还原成 SQL,同步到下游数据库。TiDB支持多源多目的地的数据同步,通过Wormhole的工具实现。PingCAP 也在研发自己的 OLAP 的存储引擎,应该是列式存储引擎,如果能推出来,那么对于实时分析数据来说就比较方便了
目前的new SQL底层普遍基于RocksDB,这是一个写优化的key-value数据库,但是读效率没有b+树高,但是可以通过增加数据服务器数量来弥补这个缺陷,机器费用的增加相比人力的投入来说忽略不计,从理论上具备了代替分库分表方案的条件。
参考文档
https://segmentfault.com/a/1190000015892186
https://www.jianshu.com/p/1ed61b4cca12
http://www.cnblogs.com/yanghuahui/p/3483047.html
https://blog.csdn.net/u010842515/article/details/68929687
https://www.jianshu.com/p/6636e4671f83
https://www.jianshu.com/p/6f68d3c118d6
https://www.jianshu.com/p/3832ae37fac4
https://blog.csdn.net/doc_sgl/article/details/51068131
https://www.oschina.net/translate/how-to-do-performance-tuning-on-tidb