工作中每天都要用到数据库,但老实讲对于数据库的了解一直都处在一个很模糊的状态,锁机制知道一些,事务也了解一些,但这些都没有统一起来。平时在工作中一直也是不求甚解,会CRUD就行,但这样终究是不对的,不论这对工作是否有影响,但人生态度不该如此。所以这篇文章想将这些零散的知识先收集起来,日后也好深入学习。数据库真的很复杂,个人能力也十分有限,所以该文章更多是一篇科普性文章,各位担待。此外,这篇文章是关于InnoDB的,别的我也不会。
数据库系统定义
虽然不是文科,但有时候牢记一些精确的描述也并非多此一举,至少显得我们专业。
数据库系统是一些互相关联的数据以及一组使得用户可以访问和修改这些数据的程序的集合。——《数据库系统概论》
在MySQL中,其逻辑结构大体可以分为三部分:
- 第一部分负责的是与客户端之间的连接管理。包括连接维护、安全认证、授权等。MySQL会为每一个客户端连接维护一个线程,而每个客户端发出的SQL查询都会在对应的线程中执行。
- 第二部分是MySQL的核心部分。包括了对SQL语句的解析、分析、缓存和优化等功能,还有聚集函数、触发器、视图等也都在这里实现。select操作会先检查是否命中查询缓存,命中则直接返回缓存数据,否则解析查询并创建对应的解析树。
- 第三部分就是MySQL实际负责存储和提取的存储引擎。MySQL通过API来调用存储引擎,这样就屏蔽了各种引擎之间的差异。常见的存储引擎有:InnoDB、MyISAM。
下面是一张从网上截下的逻辑结构图,该图将上面讲的第一部分和第二部分画在了一起,但这没关系:
另外,启动一个MySQL服务时,会有一个mysqld的进程,这个就是提供数据库服务的守护进程,默认监听3306端口。而我们平时在终端使用的mysql -u root命令则是启动MySQL的客户端,它会与mysqld建立连接。
InnoDB的逻辑存储结构
数据库由一个或多个称作表空间的逻辑存储单元组成。表空间表空间,顾名思义,用来存放表的空间,我个人将其理解成类似命名空间的东西,在InnoDB中,一个表空间就是一个ibd文件(有一个ibdata1文件,是一个公用表空间,里面存储了一些公用表数据)。表空间之下又可以划分为段、区、页。下面是一张取自《MySQL技术内幕》的图片:
当开启了innodb_file_per_table后,每张表都会有个一自己的表空间table_name.ibd。没有开启这个参数时,InnoDB中的所有表会存到共享表空间ibdata1里。一个表空间可以包含一张或多张表(这取决于是否开启innodb_file_per_table)。在InnoDB中,数据即索引,索引即数据(这是在说聚集索引),所以一张表的数据可以看作是一个主B+树和若干其它B+树(因为一张表可能有聚集索引和辅助索引),所以,一个ibd文件里存储的就是该表的若干B+树(对于表空间的理解,我不是很有把握,如果有误,望指正)。
表空间又是由段组成,分数据段、索引段等。数据段由B+树的叶子节点构成,索引段由B+树的非叶子节点构成。
页是数据库里最小的存储单元,页里面存储的就是一行行的记录,同时它也是构成B+树的结点。要注意的是InnoDB在查找对应的记录时,并不会直接从B+树中找出对应的行记录,它只能获取记录所在的页,然后将整个页加载到内存中,再通过 Page Directory (页目录)中存储的稀疏索引和 next_record 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。比如一颗3层的B+树,要查询出一条记录,则需要3次IO,每次读取一层中的某一页。
索引
索引主要分为两种:聚集索引和辅助索引。
聚集索引
如果包含记录的文件按照某个搜索码指定的顺序排序,那么该搜索码对应的索引称为聚集索引。——《数据库系统概念》
前文说过“数据即索引,索引即数据”,说的就是聚集索引。在InnoDB中,一张表的数据一定是按照其某列或某些列的值来顺序组织的,而该列就是这张表的聚集索引。又因为InnoDB会在聚集索引的基础上构建出一颗B+树,这颗B+树的叶子结点存储的就是那些组织好的记录,所以我们说“数据即索引,索引即数据”。这一点一定要记住,这颗B+树的叶子结点存放的都是按照聚集索引排好序的记录。在InnoDB中,聚集索引即主键(虽然聚集索引通常是主键构成,但并非必须如此,其实可以指定任何列,只不过在InnoDB中是这样的),所以在每个ibd文件里一定至少存储着一个以主键构成的B+树,而这颗B+树就是我们的表(开启innodb_file_per_table)。
对于主键,如果在创建表时没有显示创建主键,则InnoDB会判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列为主键,如果没有则InnoDB会自动创建一个主键row_id。
上图中,Key就是主键,data就是对应的每行记录,而我们的表就是以这样的B+树存在磁盘中。
另外一提的是,虽然B+树整体是个有序结构,但其实B+树结点(页)里的记录是无序存放的(上图中每页的记录都是有序,这只是一个示意,真实情况下可能是无序存放的),这些记录是通过每个记录里的next_record属性串起来形成一个有序链表。至于说为什么主键最好设置为自增,那是因为如果无序的插入数据的话,很容易破坏已有的B+树结构,这样InnoDB为了维护B+树,会造成额外的开销;而如果将主键设置为自增后,记录只会依次往B+树的最后一个结点增加,当一页满了后,InnoDB只用往后再申请个结点(页)即可,这样对性能的影响相对较小。
辅助索引
搜索码指定的顺序与文件中记录的物理顺序不同的索引称为非聚集索引或辅助索引。——《数据库系统概论》
跟聚集索引一样,辅助索引最后也是以某列或某些列为基础构建出一颗B+树,它会跟主键B+树一起存放在ibd文件里。不同的是,辅助索引的B+树其叶子结点存放的不是整个记录,而是对应的主键值和辅助索引本身的值。可以这么想,将表的所有主键抽出来,然后按其对应辅助索引的大小顺序组织,然后构成一颗B+树。
在通过辅助索引查询的时候,会先通过这个辅助B+树查询到对应的主键,然后拿着主键去聚集B+树里查询出整条记录,这称之为回表查询。当然,如果要查询的字段就是辅助索引里的某个,那这时就不用回表了,比如有辅助索引(A,B),而你通过字段A想查询字段B,因为在辅助B+树的叶子结点里就有这些值。
事务
构成单一逻辑工作单元的操作集合称作事务。即使有故障,数据库系统也必须保证事务的正确执行——要么执行整个事务,要么属于该事务的操作一个也不执行。此外,数据库系统必须以一种能避免引入不一致性的方式来管理事务的并发执行。——《数据库系统概论》
原子性
一个事务里的步骤集合必须作为一个单一的、不可分割的单元出现。其必须保证要么执行全部内容,要么就根本不执行。
隔离性
由于事务是一个单一的单元,它的操作不能看起来是被其它不属于该事务的数据库操作分隔开的,至少在用户面前,一个事务要表现出是连贯的。换句话说,一个事务在执行的过程中,要确保没有被别的来自并发的数据库操作干扰。当然,出于性能的考虑,事务之间往往都是交错执行的,所以它不能保证绝对意义上的隔离,而是要看所采取的隔离等级,这是隔离与性能之间的权衡。
持久性
即使系统崩溃,已提交的事务,其操作也必须是持久的。
一致性
前面说的三个特性其实是属于事务本身具有的特性。而一致性更多的是强调程序员自己的职责。任何一个单一的事务,必须保证其操作后数据库里的数据依旧保持一致。不能说在一张表做了加法,对应另外一张表却忘了做减法。
隔离级别
这个就是针对隔离性来说的了,在此之前必须强调一些关键的概念。”调度“的意思是指数据库系统以怎样的顺序来执行多个事务,可以一个一个的事务串行执行,也可以多个事务之间交叉执行。
保证所执行的任何调度都能使数据库处于一致状态,这就是数据库系统的任务,数据库系统中负责完成此任务的是并发控制部件——《数据库系统概论》
换句话来说,并发控制的任务就是让并发的事务像是没有在并发一样。当然,不同的隔离级别不能保证这一点。
1.可串行化
当所执行的调度,其效果与没有并发执行的调度效果一样,这种调度称为可串行化调度。可以看出,可串行化并不是说事务真的是串行执行的。
虽然”可串行化“并非完全是串行执行的,但它的并发度依旧很小,所以SQL标准允许一个事务以一种与其它事务不可串行化的方式执行,也就是将要介绍的另外3个隔离级别。
2.可重复读
只允许读取已提交数据,而且在一个事务两次读取一个数据项间,其它事务对该数据项的更新对该事务来说是不可见的。
3.已提交读
只允许读取已提交数据,但不要求可重复读。比如,在事务两次读取一个数据项期间,另一个事务更新了该数据并提交。
4.未提交读
允许读取未提交数据。
以上所有隔离性级别都不允许脏写,即如果一个数据项已被另外一个尚未提交或终止的事务写入,则不允许对该数据项执行写操作,言下之意就是说任何写操作都要加上互斥锁。实际上,事务的隔离性是要解决”读“与”写“之间的问题,而”写”与“写”之间不管怎样都是互斥的。
事务的并发控制
在Mysql中有三种常见的并发控制方法,分别是悲观并发控制、乐观并发控制和多版本并发控制,其中悲观并发控制其实是最常见的并发控制机制,也就是锁;而乐观并发控制其实也有另一个名字:乐观锁,乐观锁其实并不是一种真实存在的锁;最后就是多版本并发控制(MVCC)了,与前两者对立的命名不同,MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。
每一个事务都是由若干个读和写指令组成的,在理想的情况下,这些事务应该是串行执行的。但因为效率的考量,事务与事务之间往往都是并发执行的。如果每个事务都是由读指令组成的,那自然不用考虑什么并发问题了;如果每个事务都是由写指令组成的,其实也没什么好担心的,即使这些事务穿插着执行,但由于所有的写指令都必须加互斥锁,所以不会出现什么奇怪的问题。但若多个事务里有读有写,情况就要复杂一些,其实只要给这些读操作也加上锁也就没问题了,但这样就牺牲了性能,但若没给其中的读操作加上锁,则会出现在同一个事务里同样的读操作会有不同结果的问题。
MVCC就是在对一个事务里的读操作不加锁的情况下,确保数据多次读取的一致性,对应的就是“可重复读”和“读已提交”。实际上MVCC解决了这样的一个问题:多个事务并发执行,其中一个事务不希望对读操作上锁,同时还希望它所要读取的数据不会受别的事务影响。但MVCC并不是万灵药,大量的业务问题的关键点在于,你在提交一个事务那一刹那,你提交事务的所有修改依赖的读取是否都还有效。对于这种场景,无论是Read Committed还是Repeatable Read都没有什么卵用,比如扣库存就是这样典型的业务场景。在这种场景下,使用者会使用select ... for update手工加锁,或者干脆用Serializable隔离级别。也可以使用乐观锁。