InnoDB 引擎

01 | 基础架构:一条SQL查询语句是如何执行的?

概述

  1. mysql> select * from T where ID=10

大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。

(1)Server 层

包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能
以及所有的内置函数(如日期、时间、数学和加密函数等);
所有跨存储引擎的功能(存储引擎就是两大部分之一)都在这一层实现,比如存储过程、触发器、视图等。

(2)存储引擎层

负责数据的存储和提取
架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。
现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

阅读笔记___Myql45讲 - 图1

(注意)从图中不难看出,不同的存储引擎共用一个 Server 层,也就是从连接器到执行器的部分。

一、两大架构之1:Server层

1 连接器

(1)连接的建立

① 输入语句
连接器负责跟客户端建立连接、获取权限、维持和管理连接
连接命令一般是这么写的:

  1. mysql -h$ip -P$port -u$user -p

密码不建议直接跟在 -p 后面写在命令行中,这样可能会导致你的密码泄露。
③ 输入的用户名和密码。如果用户名或密码不对,你就会收到一个”Access denied for user”的错误,然后客户端程序结束执行。
④ 关于密码的修改与权限

如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。

(2)连接的空闲状态

① 空闲状态的连接
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。结果,其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。

② 连接自动断开
客户端如果太长时间没动静,连接器就会自动将它断开。
这个时间是由参数 wait_timeout 控制的,默认值是 8 小时
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。

③ 长连接和短连接
长连接:是指连接成功后,如果客户端持续有请求,则一直使用同一个连接
短连接:是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个
建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接

④ 长连接内存问题以及解决方法

  1. 但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 **MySQL 在执行过程中临时使用的内存是管理在连接对象里面的**。
  2. 这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。
  3. 这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,**被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了**。
  4. 怎么解决这个问题呢?你可以考虑以下两种方案。<br /> <1> **定期断开长连接**。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。<br /> <2> 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来**重新初始化连接资源**。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

8.0已废弃:查询缓存(一般不用)

连接建立完成后,你就可以执行 select 语句了。
执行逻辑就会来到第二步:查询缓存。

(1)执行缓存的过程

  1. MySQL 拿到一个查询请求后,会**先到查询缓存看看,之前是不是执行过这条语句**。<br /> **之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。**key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。**执行完成后,执行结果会被存入查询缓存中**。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。

(2)查询缓存的弊端

  1. **查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空**。<br /> **对于更新压力大的数据库来说,查询缓存的命中率会非常低**。<br /> 除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。

(注意)需要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了

2 分析器

如果没有命中查询缓存,就要开始真正执行语句了。

(1)词法分析

  1. 首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。**分析器先会做“词法分析”**。<br /> 你输入的是由多个字符串和空格组成的一条 SQL 语句,**MySQL 需要识别出里面的字符串分别是什么,代表什么**。<br /> MySQL 从你输入的 "select" 这个关键字识别出来,这是一个查询语句。它也要把字符串 T 识别成 “表名 T ,把字符串“ID”识别成 “列 ID”。

(2)语义分析
做完了这些,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法
如果你的语句不对,就会收到 “ You have an error in your SQL syntax ” 的错误提醒。
一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容。

3 优化器

  1. 经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。<br /> 一个语句可能有多种执行方法,并且**逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案**。<br /> 优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。

4 执行器

(1)权限判断
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限。如果没有,就会返回没有权限的错误。

(2)执行
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
执行器将所有满足条件的行组成的记录集作为结果集返回给客户端

(2)关于引擎提供的接口
至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。
第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口这些接口都是引擎中已经定义好的
你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

二、两大架构之1:存储引擎

未讲。

课后问题

  1. 留一个问题吧,如果表 T 中没有字段 k,而你执行了这个语句 select * from T where k=1, 那肯定是会报“不存在这个列”的错误: Unknown column k in where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢?

==> 分析器,词义分析阶段报错。

评论精华

问题1

  1. 问:我创建了一个没有select权限的用户,执行select * from T where k=1,报错“select command denied”,并没有报错“unknown column”,是不是可以说明是在打开表之后才判断读取的列不存在?
  2. 1(作者):这个是一个安全方面的考虑。你想想一个用户如果没有查看这个表的权限,你是会告诉他字段不对还是没权限?如果告诉他字段不对,其实给的信息太多了,因为没权限的意思还包含了:没权限知道字段是否存在😄
  3. 2:**这个权限验证是在分析器阶段之后和优化器之前完成的** 分析器有两个过程:词法分析 语法解析 词法分析过程拆分语句根据钩词规则识别单词, 语法解析过程判断SQL是否满足MySQL语法,生成语法树。 上面两步都没有打开表,也没有检查权限,在语法解析之后其实还有一个步骤,这一步骤里会根据一些MySQL规则进一步检查上面生成的语法树,如检查是否有表权限,表名、列名是否正确等 在你提到的问题中,报错主要发生在这个阶段,检查发现没有权限,那么报错无权限,如果是一个有权限的用户,会进一步提示列不存在。 后面那个解析过程有些文章里单独提出来称为“处理器”

三、更新逻辑

12 | 为什么我的MySQL会“抖”一下?

一条 sql 有时莫名其妙很慢.
小结:延续02中,WAL概念:
机制后续解释(刷脏页操作,执行时间)
带来的“内存脏页”问题

一、SQL 变慢

1 回顾 WAL:先写日志,不忙时,再写磁盘。

内存数据写入磁盘:flush
flush前,“内存”与“磁盘”数据不一致。

2 “脏页”与“干净页”
“内存”与“磁盘”数据 一致:干净页
不一致:脏页

3 图示“孔乙己赊账”

4 “抖”的原因
正常:写内存与日志.
“抖”:刷脏页!

5 什么情况引发 flush?
① redo log 满了.
② 内存满了,并“淘汰脏页”时.(这里逻辑没太懂)
③ 空闲时.
④ 正常关闭 mysql 时.

6 上述 4 种场景对性能的影响
③④不用太关注.
① 应该尽量避免!此时所有“更新”都进行不了!
② 是常态!Innodb 种 Buffer Pool 管内存(没用的 / 干净页 / 脏页)

总结影响性能的情况:要处理的脏页太多,即② + 日志满了,即①,这种不可接受!
==> 下节课就讲如何 “控制脏页比例”

二、“刷脏页”控制策略

1 首先告知 Innodb 所在主机的 I/O 能力(即“全力刷脏页”的能力)

2 不可能一直全力刷脏页,应控制引擎按照“全力的百分比”(别太接近 75%)来shua
影响因素:脏页比例 + redo logo 写盘速度
==> 通过 f(N) 和 f(M) 来控制. (没太细研究)

3 再看一个策略:“脏页连坐”
SSD 种建议关了,mysql 8.0 中也关了.
脏页连坐:本来只 flush 一下,但如果“旁边就是脏页”,会顺手也flush一下。导致更慢了。

13 | 为什么表数据删掉一半,表文件大小不变?

索引

04 | 深入浅出索引(上)

05 | 深入浅出索引(下)

优化索引,如何避免回表?
覆盖索引
最左前缀
索引下推

09 | 普通索引和唯一索引,应该怎么选择?

零、引

  1. 之前介绍过 普通索引 / 唯一索引.
  2. 本节介绍怎么选。介绍一个市民的例子,需求中我们需要获取 身份证号 姓名,身份证号能保证业务上的不重复。因为身份证号太长,我们也不考虑它作为主键索引。此时,我们选择普通索引还是唯一索引呢?
  3. 本节,通过 04 中的例子,说明**两种索引对 update select 的影响**。

一、查询时的区别(两类index没啥区别)

(1)查询过程

  1. 一般从B+树根开始
  2. 按层搜索到叶子节点;
  3. 数据页内,使用二分法定位记录

(2)对比结果:性能差别很小

  1. 唯一index:找到了就停,就1
  2. 普通index:找到第一个以后,一直找到不是的为止,可能有多个

(3)差异小的原因

  1. InnoDB引擎 **[按页读写]**
  2. (一般不考虑数据分布于两页的情况,一页能存储很多数据了,这种情况概率太小)

二、更新时的区别

1. 前置知识:Change Buffer

  1. 当要更新的**数据不在内存中时**,会使用到 Change Buffer

(1)更新数据项时

  1. 内存中有,那么就直接在内存中更新;
  2. 内存中没有时,在不影响一致性时(???这句话啥意思),**将更新缓存于 Change Buffer 中,无需将数据从磁盘中读入!**

(2)下次查询时,数据页从磁盘读入内存时,使用 Change Buffer 中的记录来更新数据!

(提醒) Change Buffer 于内存中有拷贝,会被写入磁盘中(?????????这句话啥意思)

2. 提高更新语句的效率方案:使用 Change Buffer

(1)可以减少磁盘的读写

  1. 更新时不用读进来,查询时读进来,再更新即可。

(2)不用占有 Buffer Pool,提高了内存利用率

  1. (啥时候细讲缓冲池啊???)

==> 提到了 merge 的概念:把 Change Buffer 中操作应用到原数据页

  1. (是不是就是查询时,把数据从磁盘中读到内存中,然后把 Change Buffer 中缓存的更新指令,在刚读入内存的数据中应用的过程???)

3. 何时使用 Change Buffer ?

(1)唯一 index:不可使用 Change Buffer

  1. 因为所有 update **必须判断该操作是否违反唯一性约束**。
  2. 如果数据不在内存中,把数据读入内存才能判断;
  3. 如果数据在内存中,直接判断。
  4. ==> 因此,**横竖都用不到 change buffer!**

(2)仅普通约束可用Change Buffer

4. Change Buffer 的设置方式

  1. Change Buffer 占用了 buffer pool 的空间,∴ **空间有限**。
  2. 一个参数…………

5. 使用 Change Buffer 时,update 语句的执行流程

  1. InnoDB 引擎执行流程.
  2. 用之前案例中的 update 语句分析

(1)情况1:更新目标在内存中(性能差别很小

① 唯一:找到数据 ——> 判断唯一性 ——> 插入

② 普通:找到数据 ——> 插入

(2)情况2:更新目标不在内存中(体现性能差异)

① 唯一:读入内存 ——> 判断唯一性 ——> 插入

② 普通:更新于 Change Buffer

三、Change Buffer 的使用场景:任何场景中, Change Buffer 均可为普通索引加速吗?

(1)适用:写多读少

(2)不适用:读多

  1. **读取就会触发触发 merge,进行 I/O**
  2. **读多时,频繁触发 merge,从而频繁触发 I/O**,效率可能比不使用更低!

四、索引的选择与实践:普通索引更好

  1. 回到文章开头的问题,普通/唯一索引怎么选?

(1)查询:两者没区别

(2)更新:普通索引更好!

  1. 读少时,正常使用
  2. **② 读多时,关闭 Change Buffer !**

五、Change Buffer 与 redo log(该部分没太看懂)

思考题

精彩评论

  1. 首先明确一个观点,redo log最大的作用,就是用于数据库异常宕机的恢复工作。 如果数据库永远不会宕机,那么不需要 redo logredo log change buffer其实关注的是两个事情,不能混为一谈。
  2. 其次,数据库缓冲池中有如下内容:数据页,索引页,插入缓冲,等等其他页(其他页目前不需要了解),数据页可以理解为叶子节点,索引页可以理解为非叶子节点,插入缓冲就是老师这节课讲的 change buffer
  3. 当做insert update delete操作时,会涉及到两方面的更新,一类是主键索引B+树,另一类的非主键索引B+树。

(1)主键索引B+树和 非主键索引中的唯一索引B+树

  1. 如果在内存中,直接更新内存;
  2. 如果不在内存,直接从数据库中读取页到内存中来,更新内存即可。

(2)非主键索引的普通索引B+树

  1. 如果树在内存中,直接更新内存;
  2. 如果不在内存中,更新change buffer,等到后面**需要使用这个树的时候**,会从磁盘中读取,然后做merge操作。
  3. 有同学问,到底是依据change buffer磁盘还是依据redo log更新磁盘,我的回答是,他们都不会直接更新磁盘,刷新磁盘的工作是innodb存储引擎中的线程去做的。redo log负责的是 异常宕机的恢复;change buffer用于 提高普通索引更新的性能。

事务 + 锁 + MVCC

03 | 事务隔离:为什么你改了我还看不见?

概述

  1. **事务就是要保证一组数据库操作,要么全部成功,要么全部失败。**<br /> MySQL 中,**事务支持是在引擎层实现的**(这一点对理解上节 **redo log 两阶段提交**很有帮助)。<br /> 你现在知道,MySQL 是一个支持多引擎的系统,但并**不是所有的引擎都支持事务**。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM InnoDB 取代的重要原因之一。

==> 老师提醒 Oracle 转过来的数据的隔离界别要改一下还是啥的,和 mysql 的默认级别不一样.

一、事务隔离级别

1. ACID

  1. **ACIDAtomicityConsistencyIsolationDurability,即原子性、一致性、隔离性、持久性)**.<br /> 今天我们就来说说其中 I,也就是“隔离性”。

2. 四种隔离级别介绍

(1)事务中的常见问题
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)不可重复读(non-repeatable read)幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

(2)隔离界别的概念

  1. 在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
  2. SQL 标准的事务隔离级别包括:**读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )**。
  3. 下面我逐一为你解释:

① 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。

② 读提交:一个事务提交之后,它做的变更才会被其他事务看到

③ 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的

  1. 当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。

④ 串行化:顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

==> 思考:可重复读 & 读提交 区别是什么?

  1. 我认为读提交没有可重复读安全。
  2. 因为读提交中,同一个事务中,看到的结果还是有可能不同!因为当前事务中,会有其他事务提交,此时提交的结果读提交还是可见的!
  3. 可重复读中,当前事务看到的结果一定和一开始的是一样的,中途别的事务提交的结果是看不到的!

3. 通过一个例子演示四种隔离级别的区别

image.png

我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。

(1)若隔离级别是“读未提交”, 则 V1 的值就是 2。
这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。

(2)若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。
事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。

(3)若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。
之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的

(4)若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。
直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。

4. 四种隔离级别的实现依据?(后面深入一下,看后面讲不讲)

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

(1)读未提交:没用视图,直接读
这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;

(2)读提交 + 可重复读 :使用 “视图”(????这里感觉讲的优点笼统了????)
“可重复读”隔离级别下,这个视图是在事务启动时创建的整个事务存在期间都用这个视图。
“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建(??一些语句不也自动启动事务吗??)的。

(3)串行化:使用 “锁”
而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
==> 我们可以看到在不同的隔离级别下,数据库行为是有所不同的。

Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。
(配置方式见文章吧)

5. 可重复读 应用场景

  1. 假设你在管理一个个人银行账户表。一个表存了账户余额,一个表存了账单明细。到了月底你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。<br /> 这时候使用 “可重复读” 隔离级别就很方便。**事务启动时的视图可以认为是静态的,不受其他事务更新的影响**。

二、事务隔离级别的实现

  1. 理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们**展开说明“可重复读”**。

1. 回滚介绍

  1. MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。<br /> **记录上的最新值,通过回滚操作,都可以得到前一个状态的值**。

2. MVCC介绍

  1. **不同时刻启动的事务会有不同的 read-view**。<br /> **同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)**。

3. 回滚日志的删除时间

  1. 回滚日志总不能一直保留吧,什么时候删除呢?<br /> 答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。<br /> 什么时候才不需要了呢?就是 **当系统里没有比这个回滚日志更早的 read-view** 的时候。

4. 为什么不建议使用 长事务?

  1. 基于上面的说明,我们来讨论一下为什么建议你尽量不要使用**长事务(感觉就是一个事务中涉及的 read-view 太多)**。

(1)回滚段的影响
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间

(2)锁资源
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。

三、事务启动,与长事务的误用

(1)两种方式

事务启动方式:
一、显式启动事务语句,begin或者start transaction,提交commit,回滚rollback;
二、set autocommit=0,该命令会把这个线程的自动提交关掉。这样只要执行一个select语句,事务就启动,并不会自动提交,直到主动执行commit或rollback或断开连接。

(2)方式二导致长事务的误用

  1. 建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
  2. 建议使用方法一,

(3)多一次交互的问题

  1. 如果考虑多一次交互问题,可以使用commit work and chain语法。<br /> autocommit=1的情况下用begin显式启动事务,如果执行commit则提交事务。如果执行commit work and chain则提交事务并自动启动下一个事务。

(4)长事务的查询

06 | 全局锁和表锁 :给表加个字段怎么有这么多阻碍?

07 | 行锁功过:怎么减少行锁对性能的影响?

一、两阶段锁协议

开发中,如何正确安排事务?
InnoDB 替换 MyISAM :因为有行锁。

(1)什么是两阶段锁协议?
需要时就加锁.
事务提交时释放锁(不是说你不想要了就直接释放)
(2)根据(1)引出一个建议
(3)再引出一个例子,其中说明优化不够,进一步进出死锁的问题

二、死锁 & 死锁检测

(1)什么是死锁?

(2)两种解决死锁的方法
① 设置超时时间(不采用)
超时时间默认时间太长;
但是要是改短了,对不死锁的线程并不友好!因此,该方法不采用!

② 死锁检测(采用)
找到死锁,回滚死锁中的一个事务(??回滚一个就行了?)

(3)死锁检测中的问题
死锁检测会产生额外负担,检测的复杂度为O(n),n太大时,CPU受不了的
引出“热点行”问题

(4)如何解决 “热点行” 问题
1
2 控制并发度
在数据库端做并发控制
中间件

3 设计上:拆分

08 | 【难】事务到底是隔离的还是不隔离的?

零、引

03中,可重复读:事务中 对其他事务提价不可见 ==> 事务不会被影响
07中,行锁:一个事务锁了一行,另一个事务便阻塞了 ==> 事务可以被锁住
==> 这两点矛盾吗?更新数据后,打开锁,得到的是更新后的数据,还是一开始的数据?

(1)事务的启动
(2)一个例子
(3)在 MySQL 里,有两个“视图”的概念:
① 一个是 view。
它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。
创建视图的语法是 create view … ,而它的查询方法与表一样。

② 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view
用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
它没有物理结构,作用是事务执行期间用来定义 “我能看到什么数据” 。

(4)
在第 3 篇文章《事务隔离:为什么你改了我还看不见?》中,我跟你解释过一遍 MVCC 的实现逻辑。
今天为了说明查询和更新的区别,我换一个方式来说明,把 read view 拆开。
你可以结合这两篇文章的说明来更深一步地理解 MVCC。

一、两种视图

二、“快照” 于 MVCC 中的工作方式:数据版本可见性规则

1. 行数据的版本参数:row_trx_id

每个事务,均有【事务ID】,事务开始时申请。
每行数据,多个版本,事务更新数据时,生成新版本。每个版本,有自己的【版本参数 row_trx_id】:哪个事务修改了这行数据,就把事务ID赋值给这个版本的 row_trx_id。

2. 当前事务的一致性视图(read-view)

每个事务,引擎构造一个【活跃事务数组 / 视图数组】:保存 事务启动瞬间, 正在 “活跃” (启动但未提交) 的 所有事务ID。
【低水位】:数组中,最小的事务 ID。 (在数组中)
【高水位】:系统中,已经创建的事务ID 的 ( MAX + 1 )。 .(不在数组中)
【当前事务的一致性视图(read-view)】 = 【活跃事务数组】 + 【高水位】

image.png

3. 数据版本可见性规则(“快照” 工作原理)

【可见性规则】 = 【版本参数 row_trx_id】 与 【当前事务的一致性视图】 的比较规则
绿色区域:低水位判断,已经提交,必可见!
黄色区域:高/低水位:得知id在黄色区域 —-> 一致性视图,判断id是不是在未提交,若在数组,不可见!

这个图,其实画的挺误导人的
需要明确一点:已提交事务(绿色区域)中的事务ID,并不都小于低水位,即未提交事务(黄色区域)!
==> 视图数组中的事务,都是没有提交的,并且ID在低水位和高水位之间;
但是,ID在低水位和高水位之间的事务,并非都是未提交的,可能有的已经提交了!
==> 视图数组中的元素(未提交)图中黄色区域(未提交 + 部分提交)子集 !!!
图中黄色区域,包含了,低水位ID和高水位ID之间,所有的事务;而非仅仅是未提交的事务

③ 此时,再来看文章中的判断准则,就好理解多了!
对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
<1> 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的
<2> 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的
<3> 如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
( 就是这个 b 最容易懵逼!结合之前的解释,轻松理解!!!)

4. 用此原理解释前面的例子

20 | 幻读是什么,幻读有什么问题?

本节默认隔离级别:可重复读 RR

字段d 无索引 全表扫描 :目标行 加锁;非目标行,加什么?本节讨论.

21 | 为什么我只改一行的语句,锁这么多?

日志

02 | 【难】日志系统:一条SQL更新语句是如何执行的?

概述. 更新语句执行流程

(1)执行语句前要先连接数据库,这是连接器的工作。

(2)前面我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表 T 上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。

(3)接下来,分析器会通过词法和语法解析知道这是一条更新语句。

(4)优化器决定要使用 ID 这个索引。

(5)然后,执行器负责具体执行,找到这一行,然后更新。

==> 与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。如果接触 MySQL,那这两个词肯定是绕不过的,我后面的内容里也会不断地和你强调。不过话说回来,redo log 和 binlog 在设计上有很多有意思的地方,这些设计思路也可以用到你自己的程序里。

一、redo log

1. 理解 redo log 的作用

赊账的故事讲解 redo log 的作用
粉板=日志 账本=本地磁盘

2. 理解数据多时的 “擦除”

check 往后的 wp 是 ‘ 日志 ’ 内容.
wp 往后的 check 是 ‘ 空白 ’ 内容.

3. crash-safe

crash:崩溃
异常重启

二、binlog

1. 为什么会有两份日志呢?

  1. 因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 **InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力**。

2. redo/bin log 的区别

(1)独有,共有,所处的架构不一样
(2)r 是物理的
(3)r 固定大小,会循环覆盖;b 不是固定大小的,不会把之前的部分覆盖

三、更新语句的完整执行过程

image.png

(1)
执行器:找引擎要东西
引擎:找数据。先去内存;内存没了,就去磁盘找到并加载到内存
最后,结果返回给执行器

(2)
执行器:执行操作,得到更新以后的结果。
调用引擎接口,输出结果

(3)
引擎:更新内存数据
同时,redolog更新这些操作的记录。redolog处于prepare状态,告知执行器,随时可以提交

(4)执行器:生成该操作的binlog,并写入磁盘

(5)
执行器:调用引擎的事务提交接口;
引擎:提交之前已经prepare的redolog,完成更新

四、redolog 的两阶段提交

1.什么是两阶段提交

  1. **将 redo log 的写入拆成了两个步骤:prepare commit**,这就是"两阶段提交"

2. 理解两阶段提交的一个细节

  1. **redolog 属于引擎,涉及的操作是要事务管理的,没提交会回滚;**<br /> binlog server层执行的,不存在事务管理,写了就不能回滚。(??是这样吗,binlog会记录引擎中涉及的事务吗???)

3. 为什么需要两阶段提交?分类讨论

(1)r b

r 完成了,事务提交了。b 还没写。 此时 r b 之间异常了

此时 r 是更新过的,b 里是更新数据前的,不匹配。

(2)b r

b 完成了更新。r 事务都没提交,回滚到更新前。
异常后
b 是更新后的,r 是回滚到更新前的,不匹配.

(3)

实际上,因为是两阶段提交,(1)的基础上:这时候 redolog 只是完成了prepare, 而 binlog 又失败,那么事务本身会回滚,所以这个库里面 status 的值是 0。即binlog没记,redolog滚回去了,所以两者都没更新

(4)疑问

(1)中,r 提交了,b 写了一半了,异常重启了,b这写了一半的,怎么恢复?b 中的恢复一半?b 中会记录 r 中的事务提交吗???
==>答疑文章,第15讲中介绍

23 | MySQL是怎么保证数据不丢的?

24 | MySQL是怎么保证主备一致的?

主从

24 | MySQL是怎么保证主备一致的?

答疑

15 | 答疑文章(一):日志和索引相关问题

30 | 答疑文章(二):用动态的观点看加锁

Mysql 实战问题

10 | MySQL为什么有时候会选错索引?

11 | 怎么给字符串字段加索引?

14 | count(*)这么慢,我该怎么办?

19 | 为什么我只查一行的语句,也执行这么慢?

小结


介绍了可能的 “被锁住”“执行慢”的例子.
涉及“表/行锁”“一致性读”


学会用文中介绍的方法.
定位并解决实际问题.

类1

例子中,大概率是表t被锁住了.
分析问题时,一般先执行 show processlist 命令(调用的循环是 xx 过程??后面看看),查看当前语句所处状态,由此分析“产生的原因”“如何复现”“如何处理”

等MDL锁(表锁)

1 情况:等待表的 元数据锁
有一个线程正于表 t 上请求或持有 MDL写锁,把 select 语句堵住了。

2 复现
A持有写锁
B需要读锁,所以一直等.

3 处理方式:找到who持有mdl锁,然后kill掉
(操作见文章)

等 flush

等行锁

类2:查询慢

结尾