这个系列的读书笔记是码农翻身的刘老师组织的读书活动,入群费 100 元,分阶段读完整本书,每阶段输出读书笔记,读完后退费。本文为本书第一章的笔记。

MySQL 的灵活性体现在很多方面,但其中最与众不同的特性是它的存储引擎架构:将查询处理以及其他系统任务和数据的存储/提取相分离。这种设计使得我们可以在使用时根据性能、特性,以及其他需求来选择数据存储的方式。

MySQL 架构整体上分为三层:第一层处理连接、权限等;第二层包含所有核心的功能,例如,查询解析、分析、优化、缓存、所有的内置函数,存储过程、触发器、视图等等。第三层则是存储引擎。服务器通过包含几十个底层函数的存储引擎 API 来与存储引擎通信,API 可以屏蔽不同存储引擎的差异,使得对上层的查询过程透明。架构如下图:

MySQL 架构

整个 MySQL 程序包括客户端和服务端,客户端和服务端通过网络进行通信。当某个客户端和服务端建立连接后,就会在服务器进程中拥有一个线程。使用线程池可以避免频繁地创建和销毁线程。

MySQL 会将一条查询语句解析成内部数据结构——解析树,然后对其进行优化:重写查询、决定表的读取顺序、选择合适的索引等。对于 Select 语句,会用整个查询语句作为 Key,去缓存中查询处理结果。

关于并发控制。MySQL 会在两个层面来进行:服务器层、存储引擎层。最经典的解决方案就是锁机制,它通过两种类型的锁组成锁系统:共享锁(读锁)、排它锁(写锁)。锁的粒度会决定系统的并发度。一种提高共享资源的并发性的方法就是让锁更具有选择性,即只锁定需要修改的部分数据,而不是所有。在给定的资源上,锁定的数据量越少,则系统的并发程度越高。

另一方面,关于锁的操作:获得锁、检查锁是否解决、释放锁等都会增加系统的开销。因此锁策略就是在锁的开销和数据的安全性之间寻求平衡,既保证数据并发安全又能提高数据库的性能。各种数据库系统一般都是实现的行级锁。MySQL 的存储引擎可以实现自己的锁策略和锁粒度。

表锁是 MySQL 中开销最小、最基本的锁策略。尽管存储引擎可以管理自己的锁,但在执行诸如 Alter table 之类的语句时,服务器会忽略存储引擎的锁,而直接用表锁。

行级锁可以最大程度地支持并发处理,同时也带来了最大的锁开销。行级锁只在存储引擎中实现,服务器层没有实现,行级锁对服务器是透明的。

关于事务。一个运行良好的事务处理系统,必须具备:

原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)

也就是常说的 ACID 特性。一一解释:

原子性:一个事务中的所有操作,要么全部提交成功,要么全部失败回滚。
一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态。
隔离性:通常来说,一个事务所做的修改在提交之前,对其他事务是不可见的。之所以说通常,因为这和事务的隔离级别是相关的。
持久性:一旦事务提交,则其所做的修改会永久保存到数据库中。

在 SQL 标准中,定义了四种隔离级别,每种都规定了一个事务中所做的修改,哪些在事务内、事务间是可见的,哪些是不可见的。隔离级别越低,系统开销越小,并发越大。四种隔离级别如下:

未提交读(READ UNCOMMITTED):一个事务可以读取其他事务未提交的修改,这也被称为“脏读”。实际中一般很少使用。

提交读(READ COMMITTED):一个事务从开始到提交之前,所做的任何修改对其他事务是不可见的。这是大多数数据库系统的默认的事务隔离级别。这个级别也被称为不可重复读,因为在一个事务中,执行两次相同的查询,可能会得到不一样的结果。例如,第二次执行时,可以读到其他事务的提交。

可重复读(REPEATABLE READ):同一个事务中,多次读取同样记录的结果是一致的。这是 MySQL 默认的事务隔离级别。另外,InnoDB 通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。

可串行化(SERIALIZABLE):强制事务串行执行,解决了幻读的问题。由于需要在读取的每一行数据上都加锁,因此存在大量的超时和锁争用的问题。在实际中很少使用,只有在非常需要确保数据的一致性且可以接受没有并发的情况下,才考虑使用。

事务隔离级别

死锁是指两个或多个事务在同一个资源上相互占用,并请求锁定对方占用的资源。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁;多个事务同时锁定同一个资源时,也会产生死锁。书中给出了一个例子:

死锁的例子

当两个事务都同时执行完了第一条更新语句时,并锁定了该行。然后两个事务都去执行第二条语句,却发现已经被对方被锁定了。所以,这两个事务都在等待对方释放锁,并且同时持有对方需要的锁,陷入死循环。死锁发生后,只有部分或完全回滚其中一个事务,才能打破死锁。InnoDB 目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。死锁的发生还和存储引擎的实现有关。

存储引擎在修改数据表时,只需要先修改表的内存拷贝,再通过事务日志将所做的修改顺序追加到磁盘上的事务日志中,而对数据表的修改可以在后台慢慢地刷回磁盘,这样提高了事务的执行效率。这种实现方式称为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。

MySQL 提供了两种支持事务的存储引擎:InnoDB 和 NDB Cluster;比较知名的第三方的存储引擎,如 XtraDB 和 PBXT 也支持事务。

MySQL 默认采用自动提交模式,即如果不显示地开始一个事务,则每个查询都被当作一个事务执行提交操作。一些会导致大量数据改变的操作,如 Alter TableLock Tables(在服务层实现,和存储引擎无关) 等语句,在执行之前,会强制执行 commit 提交当前的活动事务。

MySQL 服务器层不管理事务,由底层的存储引擎实现事务。在同一个事务中,使用多种存储引擎是不可靠的。例如,在一个事务中混用了事务型和非事务型的表,非事务型表无法完成回滚操作,会导致数据库处于不一致的状态。

Lock TablesUnLock Tables 语句是在服务器层实现的,有自己的用途,不能代替事务处理。

关于 MVCC。它是行级锁的一个变种,在很多情况下避免了锁操作,因此开销更低。因为 MVCC 并没有一个统一的实现标准,所以不同库的实现机制有所不同,但都实现了非阻塞的读操作,写操作也只是锁定必要的行。

MVCC 是通过保存数据在某个时间点的快照来实现的。不管事务执行多长时间,在一个事务中,它看到的数据都是一致的。而事务开始的时间不同,不同事务对同一张表,同一时刻看到的数据可能是不一样的。

InnoDB 实现的 MVCC 是通过在每行记录后面保存两个隐藏的列来实现。一个保存了行的创建版本号(系统版本号,system version number),一个保存了行的过期(或删除)版本号。每开始一个事务,系统版本号会自动递增。事务开始时的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号作比较。对于不同的命令,MVCC 的实现描述如下:

命令 操作
select a. 只查找版本早于(小于或等于)当前事务版本的数据行。b. 行的删除版本未定义或者大于当前事务版本号
insert 为新插入的每一行保存当前版本号作为版本号
delete 为删除的每一行保存当前版本号作为删除标识
update 插入新的一行,保存当前系统版本号作为版本号;同时将当前系统版本号保存到原来的行作为删除标识

MVCC 只在可重复读(REPEATABLE READ)、提交读(READ COMMITTED)两个隔离级别下工作。因为 未提交读(READ UNCOMMITTED)总是读取最新的数据行,而不是符合当前事务版本的数据行;可串行化(SERIALIZABLE)则会对所有读取的行都加锁。

MVCC 多出的两列使得很多数据操作不用上锁,提升了性能;不足之处是需要额外的存储空间。

InnoDB 是 MySQL 的默认事务型存储引擎,也是最重要、使用最广泛的存储引擎。通过间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入,从而防止幻读。

一般情况下,都应该使用 InnoDB 存储引擎,除非需要用到某些 InnoDB 不具备的特性。另外,不要混合使用多种存储引擎。对存储引擎的选择,可以考虑以下几个因素:事务、备份、崩溃恢复、特有的特性。当然,还要根据具体的使用场景决定使用何种存储引擎。InnoDB 对于 95% 以上的用户都是最佳选择。

三种可以转换表的存储引擎的方法:Alter Table导出与导入创建与查询

第一章主要从整体层面上讲了关于 MySQL 数据库的各个方面,这也是我第一次系统地学习 MySQL,很有收获。