先上个 MySQL 逻辑架构图
可以看出,整个架构分为两层:server 层 和 存储引擎层。其中:
整个查询流程:
建立连接 -> 查询缓存 -> 分析器(词法分析、语法分析)-> 优化器(索引选择、执行计划选择)-> 执行器(操作引擎,返回结果)
查询缓存:key 是查询的语句,value 是查询的结果。但 ⚠️ MySQL 8.0 版本后已经没有查询缓存功能了。
WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志 redo log,再写磁盘。
crash-safe:数据库发生异常重启,之前提交的记录都不会丢失。
两种日志的区别:
一条 update 语句的执行过程:
目的:让两份日志之间的逻辑一致,避免写完 redo log 后 bin log 还没写 但 数据库挂了。
实现:先写 redo log,并使其处于 prepare 状态 -> 再写 bin log -> 再使 redo log 变为 commit 状态
如何将数据库恢复到任意一个时间点的状态:
- 首先,找到最近的一次全量备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到误删表之前的那个时刻。
相关参数
- innodb_flush_log_at_trx_commit = 1 -> 每次 redo log 都直接持久化到磁盘
- sync_binlog = 1 -> 每次事务的 binlog 都持久化到磁盘
经典八股:
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
相关参数:
show variables like 'transaction_isolation';
MySQL 中的视图概念:
MySQL 中的快照:
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。
视图 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。
当前事务的一致性视图(read-view) = 视图数组 + 高水位
视图数组:InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。
数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
以 InnoDB 为例,其使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。
B+ 树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。
根据叶子节点的内容,索引类型分为主键索引和非主键索引。
查询过程:
也就是说,基于非主键索引的查询需要多扫描一棵索引树。
问题:为什么建表规范里要求 一定要有自增主键 NOT NULL PRIMARY KEY AUTO_INCREMENT ?
从性能角度看:
自增主键的插入数据模式,每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
从存储角度看:
每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
如何建立多个字段的联合索引:
ALTER TABLE 'table_name' ADD INDEX ('col1', 'col2', 'col3');
定义:根据某个索引查询另一字段时,若该字段的值已经在这个索引上,此时可以直接提供查询结果,不需要回表,即为覆盖索引。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
建立联合索引时,索引项是按照索引定义里面出现的字段顺序排序的。
只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
问题:在建立联合索引的时候,如何安排索引内的字段顺序?
第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
第二原则是,考虑空间。name 字段是比 age 字段大的 ,可以创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。
select * from tuser where name like '张 %' and age=10 and ismale=1;
索引下推示意图:
如何执行:
InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。
5.1 查询过程:select id from T where k=5
二者性能几乎相同
5.2 更新过程
前置概念:
- 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。
- 将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
- change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
- 对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。
情况1:这个记录要更新的目标页在内存中
情况2:这个记录要更新的目标页不在内存中
明显加了唯一索引的情况2,插入操作更慢。
所以,唯一索引用不上 change buffer 的优化机制。
定义:对整个数据库实例加锁。
方法:Flush tables with read lock (FTWRL)
典型使用场景:做全库逻辑备份。
官方自带的逻辑备份工具: mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
不支持事务的引擎,只能使用 FTWRL 。
既然要全库只读,为什么不使用 set global readonly=true 的方式呢?
原因有二:
- 一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,不建议使用。
- 二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
有两种表锁:普通表锁 和 元数据锁(meta data lock,MDL)。
语法: lock tables t1 read, t2 write; MDL 不需要显式使用,在访问一个表的时候会被自动加上。
事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。
如何安全地给小表加字段?
在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
两阶段锁协议:在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
原则:若事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
参考文章:
MySQL实战45讲_MySQL_数据库-极客时间