本文概述了 MySQL 的服务器架构, 各种存储引擎之间的主要区别, 以及这些区别的重要性.
MySQL 逻辑架构整体分为三层
>最上层并非 MySQL 所独有, 主要进行如连接处理, 授权认证, 安全等功能的处理.
>MySQL 大多数核心服务均在第二层架构, 包括查询解析, 分析, 优化, 缓存, 内置函数(如日期, 时间, 数学和加密等函数). 所有的跨存储引擎的功能也在这一层实现: 存储过程, 触发器, 视图等.
>最下层为存储引擎, 负责 MySQL 中的数据存储和提取. 每种存储引擎都有其优势和劣势. 服务器通过 API 与存储引擎进行通信, 这些 API 接口屏蔽了不同存储引擎间的差异. 存储引擎 API 包含几十个底层函数用于执行, 但不会去解析 SQL(InnoDB 是个例外, 他会解析外键定义, 因为 MySQL 服务器没有实现该功能); 不同引擎只会简单的响应上层服务器的请求, 而不会相互通信.
对于存储引擎, 本文以及之后的文章只对 MyISAM 和 InnoDB 进行探究.
1. 连接管理与安全性
对于每个客户端连接, 服务器都会在进程中新建一个线程处理 (如果是线程池的话, 则是分配一个空的线程), 这个连接的查询只会在这个单独的线程中执行(每个线程相互独立), 该线程只能轮流在某个 CPU 核心(多核 CPU) 或者 CPU 中运行. 服务器会负责缓存线程, 因此不需要为每个新建的连接创建或者销毁线程(线程的重用和销毁都由服务器控制).
当客户端连接到 MySQL 服务器时, 服务器需要对其进行认证, 如基于用户名, 原始主机信息和密码; 一旦连接成功, 服务器会继续验证客户端是否具有执行某个特定查询的权限.
2. 优化与执行
MySQL 会解析查询, 并创建内部数据结构(解析树), 然后对其进行各种优化, 包括重写查询, 决定表的读取顺序, 以及选择合适的索引等.
优化器并不关心表使用的是什么存储引擎, 但存储引擎对于优化查询查询是有影响的. 优化器会请求存储引擎提供容量或某个具体操作的开销信息, 以及表数据的统计信息等.
对于 SELECT 语句, 服务器会优先查询缓存(Query Cache). 如果有缓存就直接返回缓存中的结果集, 否则就执行查询解析, 优化和执行的整个过程.
具体优化措施我们之后再进行探讨.
3. 并发控制
无论何时, 只要有多个查询需要在同一时刻修改数据, 都会产生并发控制的问题. 在处理并发读或者写的时候, 可以通过实现一个由两种类型的锁组成的锁系统来解决问题. 这两种类型的锁通常被称为共享锁 (读锁) 和排它锁(写锁).
读锁是共享的, 多个客户在同一时刻可以同时读取一个资源, 互不干扰; 而写锁是排他的, 也就是说一个写锁会阻塞其他的写锁和读锁, 这样才能保证数据安全.
一种提高共享资源并发性的方式就是让锁定对象更有选择性, 尽量只锁定需要修改的部分数据而不是所有的资源. 在给定的资源上, 锁定的数据量越少, 则系统的并发程度越高, 只要相互之间不发生冲突即可.
但加锁也需要消耗资源, 如果花费大量时间和资源来管理所而不是存储数据, 那就得不偿失了. 所以需要一种锁策略, 在锁的开销和数据的安全性之间寻求平衡.
MySQL 的每种存储引擎都可以实现自己的锁策略, 其中有两种最重要的锁策略: 表锁和行锁.
表锁是 MySQL 中最基本的锁策略, 并且是开销最小的策略, 他会锁定整张表; 在特定场景中表锁可以有良好的性能. 另外写锁也比读锁有更高的优先级, 一个写锁请求可能会被插到读锁队列的前面.
行锁可以最大程度的支持并发, 但同时开销也是最大, 在 InnoDB 中实现的就是行锁(在存储引擎层实现).
4.MySQL 的 InnoDB 引擎支持事务
有关事务的描述可以参考《MyBatis:Spring 事务管理(十一)》
MySQL 服务器层不管理事务, 事务是由下层存储引擎实现的. 所以在同一个事务中, 使用多种存储引擎是不可靠的.
InnoDB 采用的是两阶段锁定协议, 即在事务执行过程中, 随时都可以执行锁定, 锁只有在执行 COMMIT 或者 ROLLBACK 的时候才会释放, 并且所有的锁是在同一时刻被释放的, InnDB 会根据隔离级别在需要的时候自动加锁.
5. 多版本并发控制
MySQL 的大多数事务性存储引擎实现的都不是简单的行级锁. 基于提升并发性能的考虑, 他们一般都同时实现了多版本并发控制(MVCC), 他可以认为是行级锁的一种变种, 在很多情况下避免了加锁操作, 所以开销更低.
MVCC 的实现, 是通过保存数据在某个时间点的快照来实现的, 不管需要执行多长时间, 每个事务看到的数据都是一致的. 根据事务开始的时间不同, 每个事务对同一张表, 同一时刻看到的数据可能是不一样的.
下面我们通过 InnoDB 的简化版行为来说明 MVCC 是如何工作的.
InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏列来实现: 一个保存了行的创建时间, 另一个保存行的过期时间(并不是实际时间值, 而是系统版本号). 每开始一个新的事务, 系统版本号就会自动递增. 事务开始时刻的系统版本号会作为事务的版本号, 用来和查询到的每行记录的版本号进行比较. 当在默认可重复读隔离级别下时:
SELECT:InnoDB 会根据以下两个条件检查每行记录:
>InnoDB 只查找版本早于当前事务版本的数据行, 这样可以确保事务读取的行, 要么是在事务开始之前已经存在的, 要么是事务自身插入或者修改过的.
>行的删除版本要么未定义, 要么大于当前事务版本号. 这可以确保事务读取到的行, 在事务开始之前未被删除.
只有符合上述两个条件的记录, 才能返回作为查询结果.
INSERT:InnoDB 为新插入的每一行保存当前系统版本号作为行版本号.
DELETE: 为删除的每一行保存当前系统版本号作为行删除标识.
UPDATE:InnoDB 为插入一行新纪录, 保存当前版本号作为行版本号, 同时保存当前系统版本号到原来的行作为行删除标识.
保存了这两个额外系统版本号, 可以使大多数读操作都可以不用加锁, 使得读数据操作很简单, 性能很好, 并且也能保证只会读取到符合标准的行. 不足之处是每行记录都需要额外的存储空间, 需要做更多的行检查工作以及一些额外的维护工作.
MVCC 只在可重复读和读写提交两个隔离级别下工作. 读未提交下总是读取最新的数据行, 而不是符合当前事务版本的数据行; 而序列化则会对所有读取的行都是加锁, 所以这两个隔离级别与 MVCC 不兼容.
6.InnoDB 存储引擎
InnoDB 的数据存储在表空间 (tablespace) 中, 表空间是由 InnoDB 管理的一个黑盒子, 由一系列的数据文件组成. 在 MySQL4.1 后, InnoDB 可以将每个表的数据和索引存放在单独的文件中.
InnoDB 采用 MVCC 来支持高并发, 并且实现了四个标准的隔离界别, 默认是可重复读, 并且通过间隙锁策略防止幻读的出现. 间隙锁使得 InnoDB 不仅仅锁定查询涉及的行, 还会对索引中的间隙进行锁定, 防止幻影行的插入.
InnoDB 表是基于聚簇索引建立的 (后面再详细介绍), 对主键查询有很高的性能. 不过他的二级索引(非主键索引) 中必须包含主键列, 所以如果主键列很大的话, 其他的所有索引都会很大.
InnoDB 内部做了很多优化, 包括从磁盘读取时间时采用的可预测性预读, 能够自动在内存中创建 hash 索引以加速读操作的自适应哈希索引, 以及能够加速插入操作的插入缓冲区等, 这些之后再具体分析其实现. 同时作为事务型的存储引擎, InnoDB 通过一些机制和工具支持真正的热备份.
7.MyISAM 存储引擎
在 MySQL5.1 及之前的版本 MyISAM 是默认的存储引擎, 他提供了大量的特性如全文索引, 压缩, 空间函数等, 但他不支持事务和行级锁, 而且崩溃后无法安全恢复. 对于只读的数据, 或者表比较小, 可以忍受修复操作的, 依然可以继续使用.
>加锁与并发: MyISAM 对整张表加锁, 读取时会对需要读到的所有表加共享锁, 写入时加排它锁. 但在表有读取查询的同时, 也可以往表中插入新的记录(并发插入).
>修复: 对于 MyISAM 表, 可以手动或者自动执行检查和修复工作, 但会造成一些数据丢失, 而且修复操作很慢.
>索引特性: 对于 MyISAM 即使是 BLOB 和 TEXT 字段也可以基于前 500 个字符创建索引. 他也支持全文索引(基于分词创建的索引), 可以支持复杂的查询.
>延迟更新索引键: 在创建表时, 如果指定了 DELAY_KEY_WRITE, 在每次修改执行完成时, 不会立刻将修改的数据写入磁盘, 而是会写到内存中的键缓冲区, 只有在清理缓冲区或者关闭表的时候才会将对应的索引块写入磁盘. 这样可以极大提升写入性能, 但遇到数据库或服务器崩溃时会造成索引损坏.
如果表创建并导入数据行不会再进行修改操作, 这时可以采用 MyISAM 压缩表(myisampack). 这样可以极大减少磁盘空间占用, 减少磁盘 I/O, 从而提升查询性能. 压缩表支持索引, 但索引也都是只读.
来源: http://stor.51cto.com/art/201901/590536.htm