@@ -650,8 +650,6 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的
650650
651651# ## 数据空间
652652
653- == TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识==
654-
655653# ### 数据存储
656654
657655系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd
@@ -685,7 +683,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记
685683
686684
687685
688- # ### 空间收缩
686+ # ### 重建数据
689687
690688重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令:
691689
@@ -709,7 +707,7 @@ MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此
709707
710708Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构
711709
712- 问题:想要收缩表空间,执行指令后整体占用空间增大
710+ 问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大
713711
714712原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1 / 16 给后续的更新使用。表在未整理之前页已经占用 15 / 16 以上,收缩之后需要保持数据占用空间在 15 / 16 ,所以文件占用空间更大才能保持
715713
@@ -732,6 +730,8 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临
732730
733731
734732
733+ == 本节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,暂时记录,后续有了新的认知后会更新知识==
734+
735735参考文章:https:// time .geekbang .org / column/ article/ 72388
736736
737737
@@ -3791,7 +3791,7 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引
37913791
37923792* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录
37933793
3794- * InnoDB 的表数据文件** 通过主键聚集数据** ,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个** 隐含字段** 作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及)
3794+ * InnoDB 的表数据文件** 通过主键聚集数据** ,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个** 隐含字段 row_id ** 作为主键,这个字段长度为 6 个字节,类型为长整形
37953795
37963796辅助索引:
37973797
@@ -3868,6 +3868,10 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的
38683868* Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组
38693869* File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性
38703870
3871+ 数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可
3872+
3873+ 数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用)
3874+
38713875
38723876
38733877***
@@ -5529,7 +5533,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多
55295533 BEGIN [WORK];
55305534 ```
55315535
5532- 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改
5536+ 说明:不填状态默认是读写事务
55335537
55345538* 回滚事务,用来手动中止事务
55355539
@@ -5614,6 +5618,26 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多
56145618
56155619
56165620
5621+ ****
5622+
5623+
5624+
5625+ #### 事务 ID
5626+
5627+ 只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作
5628+
5629+ 事务在执行过程中对某个表执行了** 增删改操作或者创建表** ,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0
5630+
5631+ 事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量:
5632+
5633+ * 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1
5634+ * 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节
5635+ * 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字
5636+
5637+ ** 聚簇索引** 的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引
5638+
5639+
5640+
56175641
56185642***
56195643
@@ -5669,6 +5693,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多
56695693
56705694### 原子特性
56715695
5696+ #### 实现方式
5697+
56725698原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态
56735699
56745700InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)
@@ -5686,16 +5712,100 @@ undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执
56865712
56875713* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去
56885714
5689- undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment
56905715
5691- rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment
56925716
5693- * 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment
5694- * MySQL5.5 开始支持 128 个 rollback segment,支持 128* 1024 个 undo 操作
5717+ 参考文章:https://www.cnblogs.com/kismetv/p/10331633.html
56955718
56965719
56975720
5698- 参考文章:https://www.cnblogs.com/kismetv/p/10331633.html
5721+ ***
5722+
5723+
5724+
5725+ #### DML 解析
5726+
5727+ ##### INSERT
5728+
5729+ 乐观插入:当前数据页的剩余空间充足,直接将数据进行插入
5730+
5731+ 悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大
5732+
5733+ 当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log 只需要针对聚簇索引记录,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作
5734+
5735+ roll_pointer 是一个指针,** 指向记录对应的 undo log 日志** ,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log
5736+
5737+
5738+
5739+ ***
5740+
5741+
5742+
5743+ ##### DELETE
5744+
5745+ 插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据
5746+
5747+ 在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程:
5748+
5749+ * 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 ** delete mark**
5750+ * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态
5751+ * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 ** purge**
5752+
5753+ 在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,** 形成一个版本链**
5754+
5755+ 当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录:
5756+
5757+ * 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点
5758+ * 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表
5759+
5760+ 重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录:
5761+
5762+ * 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的
5763+ * 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源
5764+
5765+
5766+
5767+ ****
5768+
5769+
5770+
5771+ ##### UPDATE
5772+
5773+ 执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式
5774+
5775+ 不更新主键的情况:
5776+
5777+ * 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改
5778+
5779+ * 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程
5780+
5781+ 插入新记录时可能造成页空间不足,从而导致页分裂
5782+
5783+ 更新主键的情况:
5784+
5785+ * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表
5786+ * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中
5787+
5788+
5789+
5790+ ***
5791+
5792+
5793+
5794+ #### 回滚日志
5795+
5796+ undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment
5797+
5798+ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot
5799+
5800+ * 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment
5801+ * MySQL5.5 开始支持 128 个 Rollback Segement,支持 128* 1024 个 undo 操作
5802+
5803+ 工作流程:
5804+
5805+ * 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址
5806+ * 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot
5807+ * 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中
5808+ * 开始记录
56995809
57005810
57015811
@@ -5817,11 +5927,6 @@ undo log 主要分为两种:
58175927* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁
58185928* 以此类推
58195929
5820- 补充知识:purge 线程
5821-
5822- * 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录
5823- * purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的
5824-
58255930
58265931
58275932***
@@ -5943,7 +6048,7 @@ RC、RR 级别下的 InnoDB 快照读区别
59436048
59446049### 持久特性
59456050
5946- #### 持久方式
6051+ #### 实现方式
59476052
59486053持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。
59496054
@@ -6021,7 +6126,9 @@ MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的
60216126* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR ** 开始时** 对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的
60226127* newest_modification:每次修改页面,都将 MTR ** 结束时** 对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值
60236128
6024- 全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint并不是同一个线程
6129+ 全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程
6130+
6131+ 在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint
60256132
60266133使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值:
60276134
@@ -6046,6 +6153,10 @@ SHOW ENGINE INNODB STATUS\G
60466153* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,** 避免了随机 IO**
60476154* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn
60486155
6156+ 问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复
6157+
6158+ 解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚
6159+
60496160
60506161
60516162参考书籍:https://book.douban.com/subject/35231266/
0 commit comments