《Mysql是怎样运行的》补充

 19 19章 从猫爷被杀说起-事务简介

19.1 事务的起源

19.1.1 原子性(Atomicity

19.1.2 隔离性(Isolation

其它的状态转换不会影响到本次状态转换,这个规则被称之为 隔离性

19.1.3 一致性(Consistency

如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合 一致性 的

如何保证数据库中数据的一致性?

数据库本身能为我们保证一部分一致性需求(就是数据库自身可以保证一部分现实世界的约束永远有效)。

我们知道 MySQL 数据库可以为表建立主键、唯一索引、外键、声明某个列为 NOT NULL 来拒绝 NULL 值的插入。比如说当我们对某个列建立唯一索引时,如果插入某条记录时该列的值重复了,那么 MySQL 就会报错并且拒绝插入

更多的一致性需求需要靠写业务代码的程序员自己保证

每一笔交易完成后,都需要保证整个系统的余额等于所有账户的收入减去所有账户的支出

19.1.4 持久性(Durability

当现实世界的一个状态转换完成后,这个转换的结果将永久的保留,这个规则被设计数据库的大叔们称为 持久性 。

19.2 事务的概念

事务 大致上划分成了这么几个状态:

活动的(active)

事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

部分提交的(partially committed)

当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态。

失败的(failed)

当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败的 状态。

中止的(aborted)

如果事务执行了半截而变为 失败的 状态,比如我们前边唠叨的狗哥向猫爷转账的事务,当狗哥账户的钱被扣除,但是猫爷账户的钱没有增加时遇到了错误,从而当前事务处在了 失败的 状态,那么就需要把已经修改的狗哥账户余额调整为未转账之前的金额,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。

提交的(committed)

当一个处在 部分提交的 状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了 提交的 状态。

19.3 MySQL中事务的语法

19.3.1 开启事务

BEGIN [WORK];

START TRANSACTION;

READ ONLY

READ WRITE

WITH CONSISTENT SNAPSHOT

如果我们不显式指定事务的访问模式,那么该事务的访问模式就是 读写 模式

19.3.2 提交事务

COMMIT [WORK]

19.3.3 手动中止事务
ROLLBACK [WORK]
19.3.4 支持事务的存储引擎
MySQL 中并不是所有存储引擎都支持事务的功能,目前只有 InnoDB NDB 存储引擎支持
19.3.5 自动提交
MySQL 中有一个系统变量 autocommit
19.3.6 隐式提交
定义或修改数据库对象的数据定义语言( Data definition language ,缩写为: DDL )。
所谓的数据库对象,指的就是 数据库 视图 存储过程 等等这些东西。当我们使用 CREATE 、ALTER 、 DROP 等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务
隐式使用或修改 mysql 数据库中的表 当我们使用 ALTER USER CREATE USER DROP USER GRANT RENAME USER REVOKE SETPASSWORD 等语句时也会隐式的提交前边语句所属于的事务
事务控制或关于锁定的语句
当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会隐式的提交上一个事务
加载数据的语句
比如我们使用 LOAD DATA 语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务
关于 MySQL 复制的一些语句
使用 START SLAVE STOP SLAVE RESET SLAVE CHANGE MASTER TO 等语句时也会隐式的提交前边语句所属的事务。
其它的一些语句
使用 ANALYZE TABLE CACHE INDEX CHECK TABLE FLUSH LOAD INDEX INTO CACHE OPTIMIZETABLE 、 REPAIR TABLE RESET 等语句也会隐式的提交前边语句所属的事务
19.3.7 保存点
SAVEPOINT 保存点名称;
当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词 WORK SAVEPOINT 是可有可无的):
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称
20 20 章 说过的话就一定要办到 -redo 日志(上)
20.2 redo 日志是个啥
redo 日志占用的空间非常小
存储表空间 ID 、页号、偏移量以及需要更新的值所需的存储空间是很小的,关于 redo 日志的格式我们稍后会详细唠叨,现在只要知道一条 redo 日志占用的空间不是很大就好了。
redo 日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO
20.3 redo 日志格式
redo 日志本质上只是 记录了一下事务对数据库做了哪些修改
20.3.1 简单的 redo 日志类型
记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥就好了
20.3.2 复杂一些的 redo 日志类型
把一条记录插入到一个页面时需要更改的地方非常多
物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。
逻辑层面看,在系统奔溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子。
20.3.3 redo 日志格式小结
redo 日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来
20.4 Mini-Transaction
20.4.1 以组的形式写入 redo 日志
20.4.2 Mini-Transaction 的概念
把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr ,比如上边
所说的修改一次 Max Row ID 的值算是一个 Mini-Transaction ,向某个索引对应的 B+ 树中插入一条记录的过程也算是一个 Mini-Transaction 。通过上边的叙述我们也知道,一个所谓的 mtr 可以包含一组 redo 日志,在进行奔溃恢复时这一组 redo 日志作为一个不可分割的整体
20.5 redo 日志的写入过程
20.5.1 redo log block
20.5.2 redo 日志缓冲区
写入 redo 日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,翻译成中文就是 redo日志缓冲区 ,我们也可以简称为 log buffer
20.5.3 redo 日志写入 log buffer
log buffer 中写入 redo 日志的过程是顺序的,也就是先往前边的 block 中写,当该 block 的空闲空间用完之后再往下一个block 中写
21 21 章 说过的话就一定要办到 -redo 日志(下)
21.1 redo 日志文件
21.1.1 redo 日志刷盘时机
log buffer 空间不足时
事务提交时
后台线程不停的刷刷刷
正常关闭服务器时
做所谓的 checkpoint 时(我们现在没介绍过 checkpoint 的概念,稍后会仔细唠叨,稍安勿躁)
其他的一些情况
21.1.2 redo 日志文件组
这些文件以 ib_logfile[数字] 数字 可以是 0 1 2 ... )的形式进行命名
21.1.3 redo 日志文件格式
log buffer 本质上是一片连续的内存空间,被划分成了若干个 512 字节大小的 block log
buffer 中的 redo 日志刷新到磁盘的本质就是把 block 的镜像写入日志文件中
21.2 Log Sequeue Number
为记录已经写入的 redo 日志量,设计了一个称之为 Log Sequeue Number 的全局变量,翻译过来就是: 日志序列号 ,简称 lsn
每一组由 mtr 生成的 redo 日志都有一个唯一的 LSN 值与其对应, LSN 值越小,说明redo日志产生的越早
21.2.1 flushed_to_disk_lsn
当有新的 redo 日志写入到 log buffer 时,首先 lsn 的值会增长,但 flushed_to_disk_lsn 不变,
随后随着不断有 log buffer 中的日志被刷新到磁盘上, flushed_to_disk_lsn 的值也跟着增长。 如果两者的值相同时,说明log buffer 中的所有 redo 日志都已经刷新到磁盘中了
21.2.2 lsn 值和 redo 日志文件偏移量的对应关系
21.2.3 flush 链表中的 LSN
我们知道一个 mtr 代表一次对底层页面的原子访问,在访问过程中可能会产生一组不可分割的 redo 日志,在mtr 结束时,会把这一组 redo 日志写入到 log buffer 中。除此之外,在 mtr 结束时还有一件非常重要的事情要做,就是 把在 mtr 执行过程中可能修改过的页面加入到 Buffer Pool flush 链表
flush 链表中的脏页是按照页面的第一次修改时间从大到小进行排序的
flush 链表中的脏页按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的 LSN 值进行排序,被多次更新的页面不会重复插入到 flush 链表中,但是会更新newest_modification属性的值
21.3 checkpoint
redo 日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用redo 日志恢复该页面了,所以该 redo 日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用 。也就是说: 判断某些 redo 日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里
凡是在系统 lsn 值小于该节点的 oldest_modification 值时产生的 redo 日志都是可以被覆盖掉的
上述关于 checkpoint 的信息只会被写到日志文件组的第一个日志文件的管理信息中
21.3.1 批量从 flush 链表中刷出脏页
21.3.2 查看系统中的各种 LSN
21.4 innodb_flush_log_at_trx_commit 的用法
如果有的同学对事务的 持久性 要求不是那么强烈的话,可以选择修改一个称为 innodb_flush_log_at_trx_commit 的系统变量的值
21.5 崩溃恢复
21.5.1 确定恢复的起点
最近发生的那次 checkpoint 的信息
21.5.2 确定恢复的终点
21.5.3 怎么恢复
使用哈希表 根据 redo 日志的 space ID page number 属性计算出散列值,把 space ID page number 相同的 redo日志放到哈希表的同一个槽里
跳过已经刷新到磁盘的页面
21.6 遗漏的问题: LOG_BLOCK_HDR_NO 是如何计算的
22 22 章 后悔了怎么办 -undo 日志(上)
22.1 事务回滚的需求
22.2 事务 id
22.2.1 给事务分配 id 的时机
22.2.2 事务 id 是怎么生成的
22.2.3 trx_id 隐藏列
聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id roll_pointer 的隐藏列,如果用户没有在表中定义主键以及 UNIQUE 键,还会自动添加一个名为row_id 的隐藏列
22.3 undo 日志的格式
22.3.1 INSERT 操作对应的 undo 日志
22.3.1.1 roll pointer 隐藏列的含义
roll_pointer 本质就是一个指针,指向记录对应的 undo 日志
22.3.2 DELETE 操作对应的 undo 日志
阶段一:仅仅将记录的 delete_mask 标识位设置为 1 ,其他的不做修改(其实会修改记录的 trx_id 、roll_pointer 这些隐藏列的值)。设计 InnoDB 的大叔把这个阶段称之为 delete mark
阶段二: 当该删除语句所在的事务提交之后 ,会有 专门的线程后 来真正的把记录删除掉。所谓真正的删除就是把该记录从 正常记录链表 中移除,并且加入到 垃圾链表 中,然后还要调整一些页面的其他信息,比如页面中的用户记录数量 PAGE_N_RECS 、上次插入记录的位置 PAGE_LAST_INSERT 、垃圾链表头节点的指针PAGE_FREE 、页面中可重用的字节数量 PAGE_GARBAGE 、还有页目录的一些信息等等。设计 InnoDB 的大叔把这个阶段称之为 purge
22.3.3 UPDATE 操作对应的 undo 日志
在执行 UPDATE 语句时, InnoDB 对更新主键和不更新主键这两种情况有截然不同的处理方案
22.3.3.1 不更新主键的情况
就地更新( in-place update
先删除掉旧记录,再插入新记录
22.3.3.2 更新主键的情况
将旧记录进行 delete mark 操作
高能注意: 这里是 delete mark 操作!这里是 delete mark 操作!这里是 delete mark 操作! 也就是说在 UPDATE语句所在的事务提交前,对旧记录只做一个 delete mark 操作,在事务提交后才由 专门的线程做 purge 操作,把它加入到垃圾链表中
根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定位插入的位置)。
由于更新后的记录主键值发生了改变,所以需要重新从聚簇索引中定位这条记录所在的位置,然后把它插进去
23 23 章 后悔了怎么办 -undo 日志(下)
23.1 通用链表结构
23.2 FIL_PAGE_UNDO_LOG 页面
23.3 Undo 页面链表
23.3.1 单个事务中的 Undo 页面链表
按需分配,啥时候需要啥时候再分配,不需要就不分配
23.3.2 多个事务中的 Undo 页面链表
为了尽可能提高 undo日志 的写入效率, 不同事务执行过程中产生的 undo 日志需要被写入到不同的 Undo 页面链表中
23.4 undo 日志具体写入过程
23.4.1 段( Segment )的概念
23.4.2 Undo Log Segment Header
23.4.3 Undo Log Header
23.4.4 小结
23.5 重用 Undo 页面
23.6 回滚段
23.6.1 回滚段的概念
23.6.2 从回滚段中申请 Undo 页面链表
23.6.3 多个回滚段
一个事务执行过程中最多分配 4 Undo页面 链表,而一个回滚段里只有 1024 undo slot ,很显然undo slot 的数量有点少啊。我们即使假设一个读写事务执行过程中只分配 1 Undo页面 链表,那 1024 个undo slot 也只能支持 1024 个读写事务同时执行
不同的回滚段可能分布在不同的表空间中
23.6.4 回滚段的分类
在修改针对普通表的回滚段中的 Undo 页面时,需要记录对应的 redo 日志,而修改针对临时表的回滚段中的Undo 页面时,不需要记录对应的 redo 日志
23.6.5 为事务分配 Undo 页面链表详细过程
如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2 个回滚段。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的undo slot 就可以了
23.7 回滚段相关配置
23.7.1 配置回滚段数量
23.7.2 配置 undo 表空间
24 24 章 一条记录的多幅面孔 - 事务的隔离级别与 MVCC
24.1 事前准备
mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name | country |
+--------+--------+---------+
| 1 | 刘备 | 蜀 |
+--------+--------+---------+
1 row in set (0.00 sec)
24.2 事务隔离级别
24.2.1 事务并发执行遇到的问题
脏写( Dirty Write
脏读( Dirty Read
不可重复读( Non-Repeatable Read
如果 一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值 ,那就意味着发生了 不可重复读
幻读( Phantom
幻读 强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录
24.2.2 SQL 标准中的四种隔离级别
READ UNCOMMITTED :未提交读。
READ COMMITTED :已提交读。
REPEATABLE READ :可重复读。
SERIALIZABLE :可串行化
24.2.3 MySQL 中支持的四种隔离级别
Oracle 就只支持 READ COMMITTED 和SERIALIZABLE 隔离级别
MySQL REPEATABLE READ 隔离级别下,是可以禁止幻读问题的发生的
24.2.3.1 如何设置事务的隔离级别
使用 GLOBAL 关键字(在全局范围影响)
使用 SESSION 关键字(在会话范围影响)
上述两个关键字都不用(只对执行语句后的下一个事务产生影响)
24.3 MVCC 原理
24.3.1 版本链
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新
所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 版本链的头节点就是当前记录最新的值
24.3.2 ReadView 
对于使用 READ COMMITTED REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是: 需要判断一下版本链中的哪个版本是当前事务可见的
MySQL 中, READ COMMITTED REPEATABLE READ 隔离级别的的一个非常大的区别就是 它们生成 ReadView 的时机不同
24.3.2.1 READ COMMITTED —— 每次读取数据前都生成一个 ReadView
使用READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView
24.3.2.2 REPEATABLE READ —— 在第一次读取数据时生成一个 ReadView
只会在第一次执行查询语句时生成一个 ReadView ,之后的查询就不会重复生成了
也就是说两次 SELECT 查询得到的结果是重复的
24.3.3 MVCC 小结
从上边的描述中我们可以看出来,所谓的 MVCC Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写 写-读 操作并发执行,从而提升系统性能。 READ COMMITTD 、REPEATABLE READ 这两个隔离级别的一个很大不同就是: 生成 ReadView 的时机不同, READ COMMITTD 在每一次进行普通SELECT 操作前都会生成一个 ReadView ,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个ReadView ,之后的查询操作都重复使用这个 ReadView 就好了
说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而
是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的
24.4 关于 purge
25 25 章 工作面试老大难 -
25.1 解决并发事务带来问题的两种基本方式
并发事务访问相同记录的情况大致可以划分为 3
读-读 情况:即并发事务相继读取相同的记录。
读取操作本身不会对记录有一毛钱影响,并不会引起什么问题,所以允许这种情况的发生。
写-写 情况:即并发事务相继对相同的记录做出改动。
我们前边说过,在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过 来实现的。这个所谓的 锁 其实是一个内存中的结构,在事务执行前本来是没有锁的。
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候就会在内存中生成一个 锁结构 与之关联
方案一:读操作利用多版本并发控制( MVCC ),写操作进行 加锁
方案二:读、写操作都采用 加锁 的方式
25.1.1 一致性读( Consistent Reads
事务利用 MVCC 进行的读取操作称之为 一致性读 ,或者 一致性无锁读 ,有的地方也称之为 快照读
25.1.2 锁定读( Locking Reads
25.1.2.1 共享锁和独占锁
25.1.2.2 锁定读的语句
对读取的记录加 S锁
SELECT ... LOCK IN SHARE MODE;
对读取的记录加 X锁
SELECT ... FOR UPDATE;
25.1.3 写操作
25.2 多粒度锁
前边提到的 都是针对记录的,也可以被称之为 行级锁 或者 行锁 ,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在 表 级别进行加锁,自然就被称之为 表级锁 或者 表锁
意向共享锁,英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S锁 时,需要先在表级别加一个 IS锁 意向独占锁,英文名: Intention Exclusive Lock ,简称 IX锁 。当事务准备在某条记录上加 X锁 时,需要先在表级别加一个 IX锁
IS IX 锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS 锁和 IX 锁是兼容的, IX 锁和 IX 锁是兼容的
25.3 MySQL 中的行锁和表锁
25.3.1 其他存储引擎中的锁
25.3.2 InnoDB 存储引擎中的锁
InnoDB 存储引擎既支持表锁,也支持行锁。表锁实现简单,占用资源较少,不过粒度很粗,有时候你仅仅需要锁住几条记录,但使用表锁的话相当于为表中的所有记录都加锁,所以性能比较差。行锁粒度更细,可以实现更精准的并发控制
25.3.2.1 InnoDB 中的表级锁
手动获取 InnoDB 存储引擎提供的表 t S锁 或者 X锁 可以这么写:
LOCK TABLES t READ InnoDB 存储引擎会对表 t 加表级别的 S锁
LOCK TABLES t WRITE InnoDB 存储引擎会对表 t 加表级别的 X锁
InnoDB 的厉害之处还是实现了更细粒度的行锁
动给 AUTO_INCREMENT 修饰的列递增赋值的原理主要是两个
采用 AUTO-INC
采用一个轻量级的锁
25.3.2.2 InnoDB 中的行级锁
Record Locks
Gap Locks : 仅仅是为了防止插入幻影记录而提出的
25.3.3 InnoDB 锁的内存结构
26 26 章 写作本书时用到的一些重要的参考资料
26.1 感谢
26.1.1 一些链接
26.1.2 一些书籍

猜你喜欢

转载自blog.csdn.net/qq_35572013/article/details/128363502
今日推荐