Mysql 笔记
1. mysql的日志
1. redo log
当一条记录需要进行更新的时候,InnoDB引擎先把记录写到redo log 里面,然后更新内存(磁盘中记录的数据记录此时没有真正修改。当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为脏页。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为干净页),此时更新就完成了。InnoDB引擎会在适当的时候,将操作记录更新到磁盘里面,这个更新会挑选系统空闲的时候做,称为flush
redo log是固定大小的,比如可以配置为一组4个文件,每个文件大小是1GB,那么总共就可以记录4GB的操作。每次从头开始写,写到末尾就又回到开头循环写,如下图所示
write pos是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件write pos 和 checkpoint 之间的是还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
2. binlog
redo log是innoDB引擎自己实现的特有功能。binlog是Mysql的server层实现的功能,和引擎本身无关。
redo log和bin log的对比
- 1.redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- 2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- 3.redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

两阶段提交
上图中,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是两阶段提交。
WAL(Write-Ahead Logging)
预写日志系统。就是指的上述操作逻辑,先写redo log和bin log日志,等有空闲时间再去更新磁盘数据
3. undo log
InnoDB里面每个事务有一个唯一的事务ID,叫做transaction id,它是再事务开始的时候向InnoDB申请的,按照申请顺序严格递增。
每行数据也是有多个版本,每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id 赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id.
类似如下图所示,就是一个记录被多个事务连续更新后的状态

图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
实际上,图中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
undo log的链表结构如下图所示

2. MVCC原理
多版本的目的是为了避免写事务和读事务的互相等待,那么每个读事务都需要在不对Record加Lock的情况下, 找到对应的应该看到的历史版本。所谓历史版本就是假设在该只读事务开始的时候对整个DB打一个快照,之后该事务的所有读请求都从这个快照上获取。当然实现上不能真正去为每个事务打一个快照,这个时间空间都太高了。InnoDB的做法,是在读事务第一次读取的时候获取一份ReadView,并一直持有,其中记录所有当前活跃的写事务ID,由于写事务的ID是自增分配的,通过这个ReadView我们可以知道在这一瞬间,哪些事务已经提交哪些还在运行,根据Read Committed的要求,未提交的事务的修改就是不应该被看见的,对应地,已经提交的事务的修改应该被看到。
作为存储历史版本的Undo Record,其中记录的trx_id就是做这个可见性判断的,对应的主索引的Record上也有这个值。当一个读事务拿着自己的ReadView访问某个表索引上的记录时,会通过比较Record上的trx_id确定是否是可见的版本,如果不可见就沿着Record或Undo Record中记录的rollptr一路找更老的历史版本。如下图所示,事务R开始需要查询表t上的id为1的记录,R开始时事务I已经提交,事务J还在运行,事务K还没开始,这些信息都被记录在了事务R的ReadView中。事务R从索引中找到对应的这条Record[1, C],对应的trx_id是K,不可见。沿着Rollptr找到Undo中的前一版本[1, B],对应的trx_id是J,不可见。继续沿着Rollptr找到[1, A],trx_id是I可见,返回结果。

前面提到过,作为Logical Log,Undo中记录的其实是前后两个版本的diff信息,而读操作最终是要获得完整的Record内容的,也就是说这个沿着rollptr指针一路查找的过程中需要用Undo Record中的diff内容依次构造出对应的历史版本,这个过程在函数row_search_mvcc中,其中trx_undo_prev_version_build会根据当前的rollptr找到对应的Undo Record位置,这里如果是rollptr指向的是insert类型,或者找到了已经Purge了的位置,说明到头了,会直接返回失败。否则,就会解析对应的Undo Record,恢复出trx_id、指向下一条Undo Record的rollptr、主键信息,diff信息update vector等信息。之后通过row_upd_rec_in_place,用update vector修改当前持有的Record拷贝中的信息,获得Record的这个历史版本。之后调用自己ReadView的changes_visible判断可见性,如果可见则返回用户。完成这个历史版本的读取。
3. Mysql的锁
Mysql 有下面几种类型的锁。
- 读锁
- 行锁
- 间隙锁
- next-key lock
间隙锁只在可重复读级别存在,产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表 t,初始化插入了 6 个记录,这就产生了 7 个间隙。
这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录
也就是说这时候,在一行行扫描的过程中,不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。
锁的兼容情况如下所示
| 读锁 | 写锁 | |
|---|---|---|
| 读锁 | 兼容 | 冲突 |
| 写锁 | 冲突 | 冲突 |
但是间隙锁不一样,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了
next-key lock 间隙锁和行锁合成next-key lock,每个next-key lock都是前开后闭区间也就是说,我们的表 t 初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
4. Mysql的索引及优化
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。
每一个索引在 InnoDB 里面对应一棵 B+ 树
假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引。
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
根据叶子节点的内容,索引类型分为主键索引和非主键索引
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
根据上面的索引结构说明,我们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?
- 如果语句是
select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树; - 如果语句是
select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为 回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。那么,有没有可能经过索引优化,避免回表过程呢?
覆盖索引
如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。
最左前缀原则
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
前缀索引
MySQL 是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串mysql>altertable SUser add index index1(email);mysql>altertable SUser add index index2(email(6));
第一个语句创建的 index1 索引里面,包含了每个记录的整个字符串;而第二个语句创建的 index2 索引里面,对于每个记录都是只取前 6 个字节
5. Mysql其它相关 TODO
1. 当前读
2. 快照读
3. explain
4. order by原理
5. Id 设计
时间字段 + 随机码(可选) + 业务信息1 + 业务信息2 ……