数据库::高性能MySQL第四版

MySQL 架构

MySQL 的逻辑架构

MySQL 的架构分为三层
最上面一层是客户端,它们是大多数基于网络的客户/服务器工具或服务器需要的服务:连接处理、认证、安全,等等。
第二层包含 MySQL 的大多数功能,包括提供查询解析、分析、优化的代码,以及所有的内置函数。任何跨存储引擎提供的功能都在这一层:例如,存储过程、触发器和视图。
第三层包含存储引擎。它们负责存储和检索 “在 “MySQL 中存储的所有数据。像 GNU/Linux 的各种文件系统一样,每个存储引擎都有自己的好处和缺点。服务器通过存储引擎 API 与它们进行通信。

连接管理和安全

默认情况,每个客户端连接都有单独的处理线程,服务器缓存了就绪的线程,避免了每次连接创建和销毁的开销。
当客户端连接到服务器,服务器会进行登录信息鉴权和操作权限校验。

优化和执行

MySQL 解析查询以创建一个内部结构(解析树),然后应用各种优化。这些可能包括重写查询,确定它将读取表的顺序,选择使用哪些索引,等等。你可以通过查询中的特殊关键字向优化器传递提示,影响其决策过程。你还可以要求服务器解释优化的各个环节,以便重新修改查询、模式和设置,使一切尽可能有效地运行。
优化器并不真正关心一个特定的表使用什么存储引擎,但存储引擎确实影响到服务器如何优化查询。例如,一些存储引擎支持索引类型,这对某些查询是有帮助的。
MySQL 的内部缓存随着并发的增加成为了瓶颈,所以在 MySQL8.0 中已经彻底去掉。

并发控制

任何时候,只要有一个以上的查询需要同时改变数据,就会出现并发控制的问题。MySQL 必须在两个层面上做到这一点:服务器层面和存储引擎层面。

读写锁

处理并发读/写访问的系统通常实现一个由两种锁类型组成的锁系统。这些锁通常被称为共享锁和独占锁,或者读锁和写锁。一个资源上的读锁是共享的,或者说是相互不阻塞的:许多客户可以同时从一个资源上读取,而不会相互干扰。另一方面,写锁是排他性的–也就是说,它们同时阻止读锁和其他写锁–因为唯一安全的策略是在给定的时间内只有一个客户在向资源写,并且在客户写的时候阻止所有的读。

锁的粒度

每一个锁操作都有开销,包括获取锁、检查锁是否空闲、释放锁,等等。如果系统花费太多的时间来管理锁,而不是存储和检索数据,性能就会受到影响。管理锁是存储引擎设计中的一个非常重要的决定;将粒度固定在某一水平上可以提高某些用途的性能,但使该引擎不适合其他用途。

  • 表锁
    MySQL 中最基本的锁策略,也是开销最小的策略,是表锁。当一个客户希望对一个表进行写操作时(插入、删除、更新等),它获得一个写锁。这使得所有其他的读和写操作都无法进行。当没有人写时,读者可以获得读锁,这不会与其他读锁冲突。
    表锁在特定情况下有一些变化以提高性能。例如,READ LOCAL 表锁允许某些类型的并发写操作。写和读锁队列是分开的,写队列的优先级完全高于读队列。
  • 行锁
    提供最大并发性的锁定方式(也有最大的开销)是使用行锁。这使得服务器可以进行更多的并发写操作,但代价是必须跟踪谁拥有每一个行锁,它们被打开了多长时间,它们是什么样的行锁,以及在不再需要时清理锁的开销。
    推荐阅读:MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking

事务

事务是一组被原子化处理的 SQL 语句,是一个单一的工作单位。所有操作应该被包裹在一个事务中,这样,如果任何一个步骤失败,任何已完成的步骤都需要回滚。

事务需要满足 ACID 特性。

  • 原子性
    事务必须作为一个单一的不可分割的工作单元来运作,这样整个事务要么被应用,要么永远不会提交。当事务是原子性的,就不存在部分完成的事务:要么全部完成,要么什么都没有。
  • 一致性
    数据库应该总是从一个一致的状态移动到下一个一致的状态。
  • 隔离性
    一个事务的结果通常对其他事务是不可见的,直到该事务完成。
  • 持久性
    一旦提交,事务的变化就是永久性的。这意味着这些变化必须被记录下来,以便在系统崩溃时数据不会丢失。

隔离级别

ANSI SQL 标准定义了四个隔离级别。这个标准的目标是定义在事务内部和外部哪些变化是可见的,哪些是不可见的规则。较低的隔离级别通常允许较高的并发性,并具有较低的开销。

  • 读未提交
    在 READ UNCOMMITTED 隔离级别中,事务可以查看未提交的事务的结果。取未承诺的数据也被称为脏读。
  • 读已提交
    一个事务将继续看到在它开始后提交的事务所做的更改,而它的更改在它提交前不会被其他人看到。这个级别仍然允许所谓的不可重复的读取。这意味着你可以运行同一个语句两次而看到不同的数据。
  • 可重复读
    REPEATABLE READ 解决了 READ UNCOMMITTED 允许的问题。它保证一个事务读取的任何行在同一事务的后续读取中看起来是一样的,但理论上它仍然没解决另一个棘手的问题:幻读。简单的说,当你选择了一些行的范围,另一个事务在这个范围内插入了一条新的行,然后你再次选择相同的范围,你就会看到新的"幻影"行,这时就会发生幻读。InnoDB 和 XtraDB 通过多版本并发控制来解决幻读问题。
    REPEATABLE READ 是 MySQL 的默认事务隔离级别。
  • 序列化
    最高级别的隔离,SERIALIZABLE,通过强制事务顺序执行,使其不可能发生冲突,从而解决了幻读问题。简而言之,SERIALIZABLE 在其读取的每一行上都加了一个锁。在这个层面上,可能会出现大量的超时和锁争夺。

死锁

死锁是指两个或更多的事务在相同的资源上相互持有和请求锁,形成一个依赖性的循环。当事务试图以不同的顺序锁定资源时就会发生死锁。只要有多个事务锁定相同的资源,它们就会发生。
数据库系统实现了各种形式的死锁检测和超时。更复杂的系统,比如 InnoDB 存储引擎,会注意到循环依赖关系,并立即返回一个错误。这可能是一件好事–否则,死锁会表现为非常缓慢的查询。其他的会在查询超过锁等待超时后放弃,这并不总是好事。InnoDB 目前处理死锁的方式是回滚拥有最少独占行锁的事务。
一旦发生死锁,如果不回滚其中一个事务,死锁就不能被打破,无论是部分还是全部。它们是事务系统中的一个事实,你的应用程序应该被设计成能够处理它们。许多应用程序可以简单地从头开始重试他们的事务,除非他们遇到另一个死锁,否则他们应该是成功的。

事务日志

存储引擎可以改变其内存中的数据副本,这是很快速的。然后,将变化的记录写到持久化的事务日志中。这也是一个相对较快的操作,因为追加日志事件涉及到磁盘上一个小区域的顺序 I/O,而不是在许多地方的随机 I/O。然后,在以后的某个时间,一个进程可以更新磁盘上的表。因此,大多数使用这种技术的存储引擎(称为"write-ahead logging”)最终会将变化写入磁盘两次。如果在更新被写入事务日志之后,但在对数据本身进行更改之前发生崩溃,存储引擎仍然可以在重新启动时恢复这些变化。不同的存储引擎的恢复方法是不同的。

MySQL 中的事务

  • 理解自动提交
    默认情况下,一条 INSERT、UPDATE 或 DELETE 语句被隐含地包裹在一个事务中并立即提交。这就是所谓的 AUTOCOMMIT 模式。通过禁用这种模式,你可以在一个事务中执行一系列的语句,并在结束时提交或 ROLLBACK。
    你可以通过使用 SET 命令为当前连接启用或禁用 AUTOCOMMIT 变量。禁用自动提交时,你总是在一个事务中,直到你发出 COMMIT 或 ROLLBACK。然后,MySQL 立即启动一个新的事务。此外,在启用 AUTOCOMMIT 的情况下,你可以通过使用关键字 BEGIN 或 START TRANSACTION 开始一个多语句事务。改变 AUTOCOMMIT 的值对非事务表没有影响,这些表没有提交或回滚变化的概念。
    某些命令,当在一个打开的事务中发出时,会导致 MySQL 在执行前提交该事务。这些通常是做出重大改变的 DDL 命令,如 ALTER TABLE,但 LOCK TABLES 和其他一些语句也有这种效果。

  • 在事务中混合使用存储引擎
    MySQL 不在服务器层面上管理事务。相反,底层存储引擎自己实现事务。这意味着你不能在一个事务中可靠地混合不同的引擎。如果你在一个事务中混合使用事务表和非事务表(例如,InnoDB 和 MyISAM 表),如果一切顺利,该事务将正常工作。然而,如果需要回滚,对非事务表的修改就不能被撤销。这就使数据库处于不一致的状态,可能很难恢复,并使事务的整个意义变得毫无意义。

  • 隐式和显式锁
    InnoDB 使用一个两阶段的锁协议。它可以在一个事务中的任何时候获取锁,但是直到 COMMIT 或 ROLLBACK 才会释放它们。它在同一时间释放所有的锁。前面描述的锁机制都是隐含的。InnoDB 根据你的隔离级别自动处理锁。

    SELECT … FOR SHARE
    SELECT … FOR UPDATE

    MySQL 还支持 LOCK TABLES 和 UNLOCK TABLES 命令,这些命令是在服务器中实现的,而不是在存储引擎中。

多版本并发控制

MVCC 通过使用数据的快照工作,因为它存在于某个时间点。这意味着事务可以看到一个一致的数据视图,无论他们运行多长时间。这也意味着不同的事务可以在同一时间看到同一表中的不同数据。
InnoDB 通过为每个启动的事务分配一个事务 ID 来实现 MVCC。这个 ID 是在事务第一次读取任何数据时分配的。当一个记录在该事务中被修改时,一个解释如何恢复该修改的撤销记录被写入 undo log,并且该事务的回滚指针被指向该 undo log。
当不同的会话读取集群键索引记录时,InnoDB 会比较该记录的 事务 ID 与该会话的读取视图的对比。如果当前状态下的记录不应该是可见的(改变它的事务还没有提交),undo log 就会被跟踪和应用,直到会话到达一个符合可见条件的事务 ID。这个过程可以一直循环到一个完全删除该行的 undo log,向读取视图发出信号,表明该行不存在。

复制

MySQL 被设计为在任何给定时间在一个节点上接受写操作。这在管理一致性方面有优势,但当你需要在多个服务器或多个地点写入数据时,会导致折衷。MySQL 提供了一种原生的方式,将一个节点的写操作分配到其他节点。这被称为复制。在 MySQL 中,源节点的每个副本都有一个线程,该线程作为复制客户端登录,当发生写操作时被唤醒,发送新数据。多年来,MySQL 的复制获得了更多的复杂性。全局事务标识符、多源复制、副本上的并行复制和半同步复制是一些主要的更新。

数据文件结构

在 8.0 版本中,MySQL 将表的元数据重新设计为数据字典,与表的.ibd 文件一起包含。这使得表结构的信息支持事务和原子数据定义的改变。在操作过程中,我们不再仅仅依靠 informa tion_schema 来检索表的定义和元数据,而是引入了字典对象缓存,这是一个基于最近最少使用(LRU)的分区定义、表定义、存储程序定义、字符集和整理信息的内存缓存。服务器访问表的元数据的这一重大变化减少了 I/O,并且很有效率,特别是当表的一个子集看到最多活动,因此在缓存中最频繁时。.ibd 和.frm 文件被替换为每个表的序列化字典信息(.sdi)。

InnoDB 引擎

InnoDB 是 MySQL 的默认事务存储引擎。它被设计用于处理许多短事务,性能优秀和拥有自动崩溃恢复的能力。默认情况下,InnoDB 将其数据存储在一系列的数据文件中,这些文件统称为表空间。
InnoDB 使用 MVCC 来实现高并发,它实现了所有四个 SQL 标准隔离级别。默认的是 REPEATABLE READ 隔离级别,它有一个 next-key 锁定策略,可以防止在这个隔离级别中的幻读:InnoDB 不是只锁定你在查询中接触到的行,而是锁定索引结构中的空隙,防止幻读的发生。
InnoDB 表是建立在一个聚类索引上的,它的索引结构与大多数其他 MySQL 存储引擎的结构非常不同。因此,它提供了非常快速的主键查询。
InnoDB 有各种各样的内部优化。其中包括用于从磁盘上预取数据的预测性预读,自动在内存中建立哈希索引以实现快速查找的自适应哈希索引,以及用于加速插入的插入缓冲器。我们在本书的第四章中介绍了这些内容。

JSON 文档支持

与新的数据类型支持一起,InnoDB 还引入了 SQL 函数,以支持对 JSON 文档的丰富操作。MySQL 8 的一个进一步改进是增加了在 JSON 数组上定义多值索引的能力。通过将常见的访问模式与能够映射 JSON 文档值的函数相匹配,这一功能可以成为进一步加快对 JSON 类型的读取访问查询的强大方式。

数据字典变化

MySQL 8.0 的另一个主要变化是取消了基于文件的表元数据存储,并转向使用 InnoDB 表存储的数据字典。这一变化将 InnoDB 的所有崩溃恢复的事务性优势带到了对表的更改等操作中。这一变化虽然大大改善了 MySQL 中数据定义的管理,但也需要对 MySQL 服务器的操作进行重大改变。最值得注意的是,过去依赖表元数据文件的备份过程现在必须查询新的数据字典以提取表的定义。

原子 DDL

最后,MySQL 8.0 引入了原子数据定义变化。这意味着数据定义语句现在既可以完全成功完成,也可以完全回滚。这通过创建一个针对 DDL 的撤销和重做日志成为可能,InnoDB 依靠该日志来跟踪变化。

可靠性工程世界中的监控

确定服务水平目标

  • 几个用来确定服务水平目标的问题

    • 衡量成功的合适指标是什么?
    • 这些指标的什么值对客户和我们的业务需求来说是可以接受的?
    • 在什么时候我们被认为处于降级状态?
    • 什么时候我们处于完全失败的状态,需要尽快进行补救?
  • 服务水平指标(SLI)
    用非常简单的话来说,SLI 回答了一个问题:“我如何衡量我的客户是否满意?” 该答案从用户的角度代表了一个健康的系统。SLI 可以是业务层面的指标,如 “面向客户的 API 的响应时间”,或更基本的 “服务已启动”。你可能会发现你需要不同的指标或度量,这取决于数据的背景以及它与产品的关系。

  • 服务水平目标(SLO)
    SLO 回答了这样一个问题:“为了确保我的客户满意,我可以允许我的 SLI 最低是多少?” SLO 是我们希望在一个给定的 SLI 中被认为是健康的服务的目标范围。如果你认为正常运行时间是 SLI,那么你希望在给定的时间跨度内达到的 9 的数量就是 SLO。SLO 必须被定义为一个特定时间范围内的数值,以确保每个人对 SLO 的含义都是一致的。SLI 加上 SLO 构成了了解你的客户是否满意的基本方程式。

  • 服务水平协议(SLA)
    SLA 为以下问题提供了答案:“我愿意同意什么 SLO,有什么后果?” 一个 SLA 是一个 SLO,它已经包含在与企业的一个或多个客户(付费客户,而不是内部利益相关者)的协议中,如果该 SLA 没有得到满足,就会有财务或其他方面的惩罚。重要的是要注意,SLA 是可选的。

如何使你的客户满意

目标是定义什么是你需要做的绝对最低限度的事情,以使客户满意。达到三个九的可用性不是一个小的成就。在一整年中,三个九相当于刚刚超过八个小时,你承诺的 “九 “越多,难度就越大,团队为实现这样的承诺所花费的工程时间也就越昂贵。

AvailabilityDowntime per yearDowntime per monthDowntime per weekDowntime per day
99.999%5 mins, 15.36 secs26.28 secs6.06 secs0.14 secs
99.995%26 mins, 16.8 secs2 mins, 11.4 secs30.3 secs4.32 secs
99.990%52 mins, 33.6 secs4 mins, 22.8 secs1 mins, 0.66 secs8.64 secs
99.950%4 hrs, 22 mins, 48 secs31 mins, 54 secs5 mins, 3 secs43 secs
99.900%8 hrs, 45 mins, 36 secs43 mins, 53 secs10 mins, 6 secs1 min, 26 secs
99.500%43 hrs, 48 mins, 36 secs3 hrs, 39 mins50 hrs, 32 mins, 17 secs7 mins, 12 secs
99.250%65 hrs, 42 mins5 hrs, 34 mins, 30 secs1 hr, 15 mins, 48 secs10 mins, 48 secs