《高性能 MySQL》读书笔记(一)—— MySQL 架构

MySQL 架构

Mysql 逻辑架构

《高性能 MySQL》读书笔记(一)—— MySQL 架构

  • 客户端负责连接 MySQL 服务器的各种工具和应用程序 ( 例如 m ysql 命令行工具、JDBC 等)
  • 连接管理
    • 负责监听和管理客户端的连接及线程处理
    • 每个客户端都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个 CPU 核心或者 CUP 中允许
    • 服务器会负责缓存线程( MySQL 5.5 及以上支持线程池),因此需要为每一个连接创建或销毁线程
  • 查询缓存
    • 缓存完整的 SELECT 查询结果。每次执行查询之前先判断是否命中缓存(哈希),如果命中缓存立刻返回结果,跳过了解析、优化和执行阶段。
    • 查询缓存会跟踪查询中涉及的表,如果这些表发生变化,则跟这个表相关的缓存失效。实际上除非事只读应用,查询缓存的失效率特别高,因此在 MySQL 8.0 中移除该功能
  • 解析器:对 SQL 语句进行解析,例如语义和语法的分析和检查,以及对象访问权限检查等
  • 优化器:利用数据库的统计信息决定 SQL 语句的最佳执行方式。使用索引还是全表扫描的方式访问单个表,多表连接的实现方式等。优化器是决定查询性能的关键组件,而数据库的统计信息是优化器判断的基础。
  • 存储引擎:负责 MySQL 中数据的存储和提取

并发控制

读锁共享,互不阻塞

写锁排他,阻塞其他读/写操作

表锁

​ MySQL 中最基本,并且是锁开销最小的锁策略。它会锁定整张表,一个用户在对表进行写操作( 插入、删除、更新等 )前,必须先获得写锁,这会阻塞其他用户对表的所有读写操作。只有没有写锁时,其他执行读取操作的用户才能获得读锁,读锁之间是互不阻塞的,

​ 写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列前面

行级锁

​ 行级锁会锁定表中的特定行,可以最大程度的支持并发处理(同时也带来了最大的锁开销)。InnoDB 和 XtraDB 以及其他的一些存储引擎实现了行级锁。

​ 行级锁只在存储引擎实现,MySQL 服务器层没有实现,服务器层完全不了解存储引擎层的琐实现。

​ 所有的存储引擎都已自己的方式实现锁机制


事务

​ 事务就是一组原子性的操作,要么全部成功执行,要么全都不执行

​ 事务的 ACID 特性

  • 原子性:事务是不可分割的最小工作单元。一个事务的操作要么全部提交成功,要么全部失败回滚
  • 一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态
  • 隔离性:一个事务的修改在提交之前对其他事务是不可见
  • 持久性:一旦事务提交,其所做的修改会永久保存在数据库中,即使系统崩溃,修改的数据也不会丢失

事务隔离级别

  1. Read uncommitted 读未提交:事务中的修改,即使未提交,对其他事务也是可见的。事务可以读取到其他事务未提交的数据(脏读)
  2. Read committed 读已提交:事务中的修改在未提交之前对其他事务是不可见的,但在同一个事务中多次读取同样的数据可能会不不一致(不可重复读)
  3. Repeatable read:可重复度:该级别保证了在同一个事务中多次读取同样的记录在结果上是一致的。但当某个事务多次读取同一范围内的记录时,可能会有其他记录新添加的新纪录,导致读取结果不一样(幻读)。InnoDB 和 XtraDB 存储引擎通过多并发版本控制(MVCC)解决了幻读问题。该隔离级别也是 MySQL 的默认隔离级别
  4. **serializable 串行化:**强制事务串行执行。会在读取的每一行数据上都加上锁

《高性能 MySQL》读书笔记(一)—— MySQL 架构


死锁

​ 死锁是指两个事务在同一资源上的相互占用,并请求锁定对方占用的资源从而导致恶性循环的现象。当多个事务视试图以不同顺序锁定同一个资源时,也会产生死锁。例如:

《高性能 MySQL》读书笔记(一)—— MySQL 架构

​ Innodb 目前处理死锁的方法是:将持有最少行级排他锁的事务进行回滚


事务日志

​ 事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表数据时只需要修改其内存拷贝,再把修改行为记录到持久化在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到硬盘

​ 事务日志采用的是追加的方式,因此写日志的操作时磁盘上一小块区域内的顺序 I/O ,所以速度很快

​ 事务日志持久化后,内存中被修改的数据在后台可以慢慢刷回磁盘。目前大多数存储引擎是这样做的,我们通常称之为预写式日志,修改数据需要写两次磁盘

​ MySQL 提供了两种事务型的存储引擎:InnoDB 和 NDB Cluster,还有一些第三方的存储引擎也支持(XtraDB、PBXT)

​ MySQL 默认采用自动提交(AUTOCOMMIT)模式。也就是所,如果不是显示的开始一个事务,则每个查询都被当作一个事务执行提交操作。 1 表示 ON 启用,0 表示 OFF 禁用

《高性能 MySQL》读书笔记(一)—— MySQL 架构

​ InnoDB 采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 的时候才会被释放,并且所有的锁实在同一时刻被释放。(隐式锁定,InnoDB 会根据隔离级别在需要的时候自动加锁)

​ InnoDB 也支持通过特定的语句进行显示锁定:

  • ​ SELECT … LOCK IN SHARE MODE
  • ​ SELECT … FOR UPDATE

多并发版本控制 MVCC

​ 可以认为 MVCC 是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行

​ MVCC 的实现,是通过保存在某个时间点的快照来实现的。也就是说无论事务需要执行多长时间,看到的数据都是一致的。根据事务的开始时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

​ InnoDB 的 MVCC ,是通过在每行记录后面保存两个隐藏列来实现的。

​ 这两个隐藏列,一个保存行的创建时间,一个保存行的过期时间(删除时间),存储的是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到每行记录的版本号进行比较。

在 REPEATABLE READ 隔离级别下,MVCC 的操作:

SELECT

​ InnoDB 会根据以下两个条件检查每行记录:

​ a. InnodDB 只查找 版本号 **早于 **当前事务版本号 的数据行(也就是,行的系统版本号小于等于当前事务的系统版本号),这样可以 确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改过的

​ b. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务被读取到的行,在事务未开始之前未被删除

INSERT

​ InnoDB 为新插入的每一行保存当前系统版本号作为版本号

DELETE

​ InnoDB 为删除的每一行保存当前系统版本号作为行删除表示

UPDATE

​ InnoDB 为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除表示

​ 保存这两个额外的系统版本号,使大多数读操作不用加锁。这样设计使得数据库操作简单,性能更好,并且也能保证只会读取到复合标准的行。缺点就是每行记录都需要额为的存储空间,需要做更多的检查工作,以及一些额外的维护工作

​ MVCC 只在 REPEATABLE READ 和 READ COMMITED 两个隔离级别下工作。其他两个隔离级别和 MVCC 不兼容。因为READ UNCOMMITIED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁

​ MVCC 在 MySQL 中的实现依赖的是 undo logread view

InnoDB 存储引擎

InnoDB 是 MySQL 的默认存储引擎,也是最重要最、使用最广泛的存储引擎。

  • 被设计用来处理大量的短期事务
  • 性能高,自动崩溃恢复特性
  • 除非有非常特别的原因需要使用其他存储引擎,否则应该优先考虑 InnoDB 存储引擎
InnoDB 概览

​ InnoDB 的数据存储在**表空间(tablespace)**中,表空间是由 InnoDB 管理的一个黑盒子,由一系列的数据文件组成。

​ InnoDB 采用 MVCC 来支持高并发,并实现了四个标准的隔离级别。其默认隔离级别是 REPEATABLE READ(可重复读),并通过间隙锁(next-key locking) 策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询设计的行,还会对索引中的间隙进行锁定,防止幻影行插入

​ InnoDB 表是基于聚簇索引建立的。聚簇索引对主键查询由很高的性能。不过它的二级索引中必须包含主键列,所以如果主键列很大的话,其他所有所以都会很大。因此,若表上的索引较多的话,主键应尽可能的小。

​ InnoDB 内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够在内存中创建 hash 索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。