MySQL 高级 - 1

SQL 查询流程

  1. 连接器建立连接
1
2
3
1. 客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时,如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。

2. 使用长连接连接 mysql 后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。解决方法是:定期断开长连接或者如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,即初始化长链接,释放内存。
  1. 查询缓存
1
2
3
4
5
6
7
8
9
10
1. MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。

2. 多数情况下不建议使用缓存,因为查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。

3. 可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。

4. 对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样
select SQL_CACHE * from T where ID=10;

5. MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。
  1. 分析器进行词法分析和语法分析
1
2
1. 词法分析:例如 MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句
2. 语法分析:判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对,就会收到“You have an error in your SQL syntax”的错误提醒
  1. 优化器优化SQL
1
1. 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。原则是:尽可能扫描少的数据库行纪录,让查询更快。
  1. 执行器
1
2
3
4
5
1. 开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。注意,这时候是对 "表" 的权限进行判断,而连接器是验证用户身份。
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

2. 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。

SQL 更新流程

1
2
mysql> create table T(ID int primary key, c int);
mysql> update T set c=c+1 where ID=2;
  1. redo log

当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。redo log 在哪,他也是在磁盘上,这也是一个写磁盘的过程,但是与更新过程不一样的是,更新过程是在磁盘上随机 IO,费时。 而写 redo log 是在磁盘上顺序IO。效率要高。

redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

redo log 是 InnoDB 引擎所特有的,所以我们如果再使用 InnoDB 引擎创建表时,如果数据库发生异常重启,之前提交的记录都不会丢失。 InnoDB 正因为有了 redo log (重做日志),才有了 crash-safe 的能力(即使 mysql 服务宕机,也不会丢失数据的能力)。

  1. binlog

MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

redo log 和 binlog 的三点区别:

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。Redo log不是记录数据页“更新之后的状态”,而是记录这个页 “做了什么改动”。binlog 有两种模式,statement 格式的话是记sql语句, row 格式会记录行的内容,记两条,更新前和更新后都有。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
  1. 数据更新流程
1
2
3
4
5
6
7
8
9
10
11
1. 执行语句前要先连接数据库,这是连接器的工作。在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表 T 上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。

2. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。注意⚠️:对于更新操作,并不会更新某条记录就把某条记录查询到内存中对其做修改就行,而是将对应记录所在页都加载到内存中。

3. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

4. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。注意⚠️:这里更新到内存中是指更新内存中这条数据所在的数据页中的数据,因为在前面的步骤就已经将这条数据所在的数据页读入到了内存中,所以这里说的更新内存指的就是更新内存中对应数据页的这条数据

5. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。

6. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成.

浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行。最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是”两阶段提交”。

  1. 两阶段提交

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

1
2
3
4
5
6
update T set c=c+1 where ID=2;
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

先写 redo log 后写 binlog: 假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。

先写 binlog 后写 redo log: 如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。