一个关系型数据库是如何工作的

总览

  • 一个关系型数据库对外表现为关系模式,即数据库为许多有关联关系表的集合,每张表包含一条条记录,每条记录有唯一的标识符,称为主键。每张表通过一个叫做外键的属性与其他的表产生关联。这样多张有关联关系的表就组成了一个数据库。关系模式的另一表现就是通过一种结构化的语言SQL,进行数据的增删查改。
  • 而关系型数据库的内部表示却没有外部表示那么简单,它涉及了多个组成部分。其中包括存储、索引、查询、并发控制和故障恢复这五个组成部分。接下来我们再对这五个组成部分进行简单的介绍,并且会在后续的文章中针对不同的部分进行更深入的探讨。

存储

  • 关系型数据库的存储方式有两种,一种是基于磁盘的存储方式,即磁盘是数据存储的主要区域,大部分常见的DBMS都是基于这种模型,比如mysql, postgres,而我们这篇博文也主要介绍这种类型的DBMS。另一种是内存数据库,即内存是数据存储的主要区域,一些新型的数据库都采用了这种方式,比如voltdb, SAP HANA。为什么会出现多种存储方式的DBMS,其根本原因在于DBMS的发展是随着硬件的发展同步进行的,当硬件的发展取得某方面巨大突破的时候,就会产生新的类型的DBMS,会在某些方面比之前的DBMS有较大的提升。在上世纪70年代DBMS刚出现的时候,当时的硬件状况是:单核CPU,内存非常小且价格非常昂贵。所以那时候的DBMS采用磁盘存储的方式。然而这种DBMS存在的问题在于,磁盘访问的速度非常慢,对于这种DBMS来说,DBMS执行一个操作,仅仅只有7%的指令用于真正的工作,而其他大部的指令都用于缓冲区管理、日志和恢复、并发控制这样的工作上。但是随着硬件技术的发展,内存的大小在不断增大,价格在不断降低,CPU也由单核变成了多核。这时候我们可以把内存作为主要的存储设备,这样就诞生了内存数据库,在这种DBMS中,IO不再是这种数据库的瓶颈所在。
  • 不管是哪种类型的DBMS,我们都需要考虑的问题是数据库如何进行物理组织。其中包括文件(一个数据库由一个或多个文件组成)如何组织页(一个文件由一个或多个页组成),页如何组织一条条的记录,一条条记录应该如何在存储设备上表示。这没有一个标准的答案,我们需要了解每种方式的优缺点和实现方式,然后根据DBMS所针对的主要使用场景,选择合适的组织方式。
  • 除了要了解如何在存储设备上组织数据之外,我们还需要了解DBMS到底要记录什么。这里要记录的信息除了要存储的数据本身,还需要包括元数据(即一个数据库的系统信息)、索引、日志等其他信息,这些信息会对整个DBMS的运作起着巨大的作用。
  • 对于基于磁盘的数据库来说,我们只要把磁盘中的数据移动到内存中才能对数据进行后续的增删查改处理。这时候我们就需要缓冲区管理器,对内存缓冲区(buffer pool)进行管理。比如当我们要访问的数据不在缓冲区时,这时候缓冲区管理器会到磁盘上读取相应的数据(往往以页为单位进行读取),它需要将这个数据所在的页读到缓冲区中然后才能进行后续的操作。然而当缓冲区满了的时候,我们就需要选择在缓冲区的页进行替换,这里就涉及到了缓冲区的替换策略。

索引

  • 索引对于DBMS的作用在于加快对数据的查找。有了索引,我们就可以根据索引的信息快速定位数据,而不需从头到尾的遍历方式。在DBMS中,索引有两种常见的结构,一种是基于哈希的方式,一种是基于树的方式。它们有着不同的实现方式,适用于不同的场景。同时我们还需要去了解到底哪些地方需要用到索引
  • 基于hash的方式,需要重点考虑如何设计hash函数,以及如何解决冲突,这是设计hash的关键所在。而hash的实现方式也是多种多样的,有不可拓展的hash,也有可拓展的hash,我们需要根据不同的情况选择不同的实现方式。hash的应用也是多种多样,比如说我们可以通过hash分区,快速判断一个值是否存在。
  • 基于树的方式,这里的树往往指的是B+树。我们常常将B+树用于存储数据和快速查找我们想要的数据。我们需要了解B+的查找、插入、删除是如何实现的,一个B+树在设计时需要考虑的因素(包括节点的大小,合并的策略等待),有哪些优化B+树的方法。
  • 除了hash和B+树之外,其实我们还会用到其他的数据结构,比如说skip lists, radix tree等待,我们需要了解这些数据结构是如何实现的,以及它们适用于什么样的场景。

查询

  • SQL语句仅仅描述了我们想要什么,并没有说明如何去取得想要的东西。DBMS会根据给定的SQL语句生成一个查询计划,然后通过执行这个查询计划得到我们想要的数据。
  • 一个查询计划涉及到了我们应该如何取得数据(是顺序取数据呢,还是按照索引去取数据),针对一个操作(如join)我们应该选择哪种算法(是先排序再join?还是先通过hash进行分区,然后再进行相应的join呢?)去执行相应的操作,如何执行这个查询计划(是一个操作全部完成再进行一个操作,还是不需要全部完成就可以直接进行下一个操作)。这些问题也没有一个固定的答案,也是需要根据实际情况进行选择。
  • 对于一个SQL语句,其实是可以生成很多等价的查询计划,那我们应该如何选择查询计划呢,这里涉及到了查询优化技术。我们有哪些办法得到一个较有的查询计划呢,因为不同的查询计划执行的速度相差巨大,人们肯定都希望查询的速度越快越好。

并发控制

  • 用户对数据库进行操作的最小单位称为事务。事务具有ACID四个特性,我们需要了解这四个特性的含义,以及DBMS如何保证事务的这四个特性。其中A(原子性)和I(隔离性)和并发控制有关,而C(一致性)和D(持久性)和故障恢复技术相关。
  • 并发控制的含义是当多个事务对同一数据库同时进行操作时,如何保证运行得到的结果和多个事务顺序执行得到的运行结果相同。即我们需要通过对事务中操作的调度,得到一个正确调度顺序,从而保证操作的正确性。这里我们需要考虑的是如何判断一个调度是正确的,以及如何得到一个正确的调度。
  • 我们重点考虑如何得到一个正确的调度。首先我们需要了解隔离级别,这涉及事务的隔离性,隔离级别的含义就是多个事务之间并发执行时相互之间的隔离程度,比如说在serializable级别下,并发执行的事务感觉不到对方的存在,就像自己一个事务在单独执行一样。又比如说read uncommitted级别,当一个事务对某一个数据有修改时,该隔离级别下的事务能马上感知到这种修改。
  • 实现正确调度的方法主要有基于锁、基于时间戳和MVCC(多版本控制)这三种。对于基于锁的方法,其基本的思路在于对数据进行访问/修改时加上不同类型的锁,当一个事务访问一个带锁的数据时会根据事务行为和锁的类型进行不同的处理(继续执行/等待锁的释放),以此来保证并发的正确性。而不同的隔离级别通过加锁和解锁的时机实现。同时锁的粒度也有所不同,锁的粒度的含义是加锁对象的“大小”,加锁的对象可以是整个数据库,也可以是一个表,一行元组。锁的粒度越大,越容易实现,隔离性越好,但是其并发度会变得越差。同时基于锁的方式,可能会出现死锁的情况,即多个事务互相等待对方完成才能继续进行,我们需要解决死锁的情况。
  • 而基于时间戳的方法有多种实现方式。其中最基本的方式是给每一个事务一个时间戳,来表示事务开始的时间。而当一个事务读写一个数据时,给该数据一个时间戳,来表示该数据被读/写的时间。其基本思路在于对于不同的事务,它们之间对相同数据写写,读写,写读这些冲突操作的顺序是一定的,是存在先后顺序的。因此在生成一个新的调度时,也必须保证那些冲突操作之间的先后顺序是正确的。
  • 最后一种是基于MVCC的方式,是当前DBMS最常用的方式。其基本思路是对一个逻辑对象维护多个物理版本,当一个事务写一个对象时,对该对象创建一个新的版本。而当以一个事务读一个对象时,找到该事务时间戳所对应的物理版本。其好处在于读和写不会相互阻塞。关于MVCC的实现有几个需要注意的对方,一个是如何存储物理的历史版本数据,如何处理/什么时候处理过旧的历史版本数据,添加新版本时如何更新索引。

故障恢复

  • 故障恢复的含义是在数据库发生故障后,如何恢复数据,不让数据发生丢失的情况,保证了事务的一致性和持久性。关于故障恢复,我们需要考虑两个问题:1.记录什么信息才能使得故障有可能恢复 2.如何根据已有信息来对故障进行恢复
  • 首先我们需要了解可能发生故障的类型以及如何处理这些故障。可能存在的故障大致分为以下三类:
    1. 事务失败,比如说事务中的一条语句违反了完整性。这时候解决的办法就是撤销该事务
    2. 系统故障,比如说突然断电,导致内存中还没持久化到磁盘上的数据丢失。这是我们这里讨论的重点所在
    3. 磁盘故障,比如说一块磁盘因为各种原因坏了,磁盘上的数据发生了丢失。这时候解决的方法只有提前备份相应的数据,才能在磁盘损坏时恢复。
  • 在了解系统故障的解决方案前,我们需要了解缓冲区管理器写页的策略。即在发生系统故障时,哪些数据会丢失和缓冲区管理器写页的策略有关。即对于一个事务,什么时候将其从内存写到磁盘上。其中需要考虑两个因素:
    1. 事务发生后是否应该马上写回磁盘
    2. 事务在未完成时是否可以写回磁盘
  • 这里的缓冲区策略涉及到我们需要记录哪些信息。如果事务完成后不马上写回磁盘,则我们在故障恢复时需要重做那些已经完成但未写回磁盘的数据,这时候我们需要redo信息。而对于那些未完成就已经写回磁盘的事务,当系统故障时,我们需要撤销那些已经持久化到磁盘上的数据,这时候就需要undo信息。那么我们到底要记录什么呢,一般大部分的DBMS采用Write-Ahead Logging 策略,其含义是在更新一条数据前,将该更新操作做了什么事情先写到日志上,然后才对相应的数据进行更新,而这些日志是会被持久化到磁盘上的。所以当系统发生故障时,我们可以根据日志中的信息进行数据的恢复。当然日志不能是无限增长的,所以我们引入了一个check-point的概念,在check-point点之前的日志表示那些操作已经被更新到了磁盘上,而check-point点之后的数据表示还未更新到磁盘上,所以恢复时只需要从check-point点之后开始恢复即可。
  • 讲完了要记录什么信息以后,我们现在开始讲讲如何利用这些信息进行数据的恢复。其大致的思路就是日志从后向前撤销未完成的事务,日志从前向后重做已经完成的事务,其中的具体实现细节留到后面的文章中再进行更详细的讲解。

总结

  • 这篇文章简单的对DBMS的组成部分进行了讲解,然而许多具体的实现要等到以后的文章才能进行更深入的讲解。总的来说,数据库还在不断的发展,越来越多的新型数据库开始出现并应用起来,其中包括nosql数据库和分布式数据库等等。于此同时硬件的发展也加速了数据库的发展,许多数据库的瓶颈/不足可以通过新型的硬件得到改善,本篇文章虽然未涉及很多新的话题,但它所包含的内容代表了数据库设计的基本思路。从基于磁盘的关系型数据库出发,我们可以了解更多的数据库及其实现方式。
  • 如果你发现了什么错误,或者有什么疑问,可以在评论区提出来,欢迎大家一起进行探讨。
posted @ 2019-05-15 14:37  HDU_jackyan  阅读(593)  评论(0编辑  收藏  举报