本文介绍了 MySQL 事务相关的概念和内核原理(原文参考何登成大神的博客)。
文章目录
事务的定义
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务其实就是并发控制的基本单位。
事务的四大属性 ACID
- 原子性( A ):事务是最小单位,不可再分
- 一致性( C ):事务要求所有的 DML 语句操作的时候,必须保证同时成功或者同时失败
- 隔离性( I ):事务 A 和事务 B 之间具有隔离性
- 持久性( D ):是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)
- 其中原子性和一致性最容易被混淆,拿转账流水举例:
- 张三给李四转账100元,那数据库做的是张三扣100,李四加100,是一个事务
- 如果张三扣100成功,李四加100失败,则需要把张三扣100的操作回滚掉,回滚功能实现了原子性
- 而如果张三扣100,李四加60,这个操作成功了,原子性实现了,但是全局状态一致没有实现
- 因此,可以理解为,一致性的实现除了原子性的方面外,还有数据库以外的业务逻辑层面的要求,更准确来说是一种一致性约束
WAL(Write-Ahead Logging)
- 日志先行,所有修改在提交前,都要先写入 LOG 文件中,WAL 是实现原子性和持久性的一系列技术。
- 核心思想:把所有随机化的修改,改为串行化的日志(append 操作),顺序化的 I/O 是非常快的,并且可以使用 Block 为单位写。
- 设计理论: ARIES 协议族
- Innodb 中的日志
- redo log,用于实现 Crash 后修复,保证所有 commit 的事务都能持久化,对应原子性和持久性;
- undo log,用于实现事务执行失败后的回滚、MVCC快照读,对应原子性和隔离性;
- redo log
- 基本上都是物理日志(硬盘页面的变化记录)
- DML 操作导致的页面变化,均需要记录 redo log
- 聚簇索引/二级索引/undo 页面修改,均需要记录 redo log
- 在页面修改完成之后,在脏页刷回磁盘之前,写入 redo log
- 日志一定比数据页先写回磁盘
- 逻辑 redo log 的例子:页面初始化操作的日志,这里只需要记录操作,而不是页内容
- undo log
- 是逻辑日志,DML 操作导致的数据记录变化,均需要将记录的前镜像写入 undo log,所谓前镜像就是更新前的值
- DML 操作修改聚簇索引前,记录 undo log(二级索引不记录 undo)
- undo 先于 redo,因为记完 undo 才能确定一行数据的版本(事务ID)和回滚段地址,才能进行更新
- insert 操作
- undo 记录主键值
- redo 记录 [space_id,page_no,完整插入记录,系统列…],space_id/page_no 代表操作的页面
- delete 操作
- undo
1. Delete, 在 InnoDB 内部为 Delete Mark 操作,将记录上标识Delete Bit,而不删除记录
2. 将当前记录的系统列写入 undo (DB_TRX_ID<事务id>, ROLLBACK_PTR<回滚段地址>,...)
3. 将当前记录的主键列写入 undo
4. 将当前记录的所有索引写入 undo
5. 将 undo page 的修改,写入 redo
- redo 记录 [space_id,page_n,系统列,记录在页面中的slot....]
- update 操作(三种情况)
CASE 1: 没有修改聚簇索引键值(主键值),并且属性列长度没有任何变化
- undo
1. 将当前记录的系统列(写入 undo
2. 将当前记录的主键列写入 undo
3. 将当前 update 列的前镜像写入 undo
4. 若 update 列包含耳机索引列,则将二级索引其他未修改列写入undo
5. 将 undo page 的修改,写入 redo
- redo
1. 进行 In Place Update(本地更新),记录 update redo 日志
2. 若更新包含二级索引列,二级索引肯定不能进行 In Place Update,记录 Delete Mark + Inser Redo 日志
CASE 2: 没有修改聚簇索引键值(主键值),属性列长度发生变化
- undo
1. 将当前记录的系统列写入 undo
2. 将当前记录的主键列写入 undo
3. 将当前 update 列的前镜像写入 undo
4. 若 update 列包含耳机索引列,则将二级索引其他未修改列写入undo
5. 将 undo page 的修改,写入 redo
- redo 不能进行 In Place Update,聚簇索引和二级索引的更新,都记录 Delete Mark+Inser redo 日志
CASE 3: 修改聚簇索引键值(主键值)
- undo
1. 不能进行 In Place Update, Update=Delete Mark+Insert
2. 对原有记录进行 Delete Mark 操作,写入 Delete Mark 操作 undo
3. 将新纪录插入聚簇索引,写入 Insert 操作 undo
4. 将 undo 页面的修改,写入 redo
- redo 不能进行 In Place Update,聚簇索引和二级索引的更新,都记录 Delete Mark+Inser redo 日志
- redo / undo 例子如下图,从 undo 开始一共 5 步,记录了 1 条 undo , 3 条 redo log
隔离级别
- 事务 A 和事务 B 之间具有一定的隔离性
- 隔离性有隔离级别(4个)
- 读未提交:read uncommitted
- 读已提交:read committed
- 可重复读:repeatable read
- 串行化:serializable
- READ UNCOMMITTED(未提交读,脏读)
- 事务中的修改,即使没有提交,对其他会话也是可见的。
- 可以读取未提交的数据——脏读。脏读会导致很多问题,一般不使用这个隔离级别。
- READ COMMITTED(提交读或不可重复读,幻读)
- 这个隔离级别保证了一个事务如果没有完全成功(commit 执行完),事务中的操作对其他会话是不可见的。
- 这个隔离级别解决了脏读的问题,但是会对其他 Session 产生两次不一致的读取结果——不可重复读/幻读。
- REPEATABLE READ(可重复读)
- 一个事务中多次执行统一读 SQL, 返回结果一样。
- innodb 中使用快照读,所谓读快照就是读取当前事务更新之前的数据,避免产生“需读”。
- innodb 中使用 next-key 锁对”当前读”进行加锁,锁住行以及可能产生幻读的插入位置,阻止新的数据插入产生“幻读”。
- SERIALIZABLE(可串行化)
- 最强的隔离级别,通过给事务中每次读写都枷锁,保证不产生幻读问题,但是会导致大量超时以及锁争用问题。
SQL 语法
mysql> start transaction; # 手动开启事务
mysql> begin; # 手动开启事务
mysql> commit; # commit 之后即可改变底层数据库数据
mysql> rollback; # 回滚事务
# 设置当前会话的隔离界别
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 设置全局的隔离级别
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
回滚
- 事务 A 执行过程中遇到异常,无法继续直行,为了实现一致性,必须把前面修改的数据恢复到事务执行前的状态,也就是回滚
- Innodb 利用 undo log(回滚日志)实现回滚,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。
- 回滚日志除了能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
Innodb 回滚段
- Innodb 使用回滚段实现 undo,是它能超越 PG 的关键(PG 用了时间戳技术),也是相对复杂的技术
- Rollback Segment 内部管理结构(4 级)
- Transaction System Header Page
- Rollback Segment Header Page
- Undo Log Header Page
- Normal Undo Page
- Transaction System Header Page (元数据页)
- 系统表空间的第五个 Page[TRX_SYS_SPACE,TRX_SYS_PAGE_NO]
- 存储着:回滚段段头页地址、MySQL Binlog信息、Double Write信息、等
- InnoDB 系统中最为重要的一个页面
- 作用:恢复时,哪些事务需要回滚; 启动后,第一个事务ID的分配起点; 运行中,回滚段的管理
- Rollback Segment Header Page (回滚段资源 undo slot 管理)
- 回滚段段头页
- InnoDB支持多回滚段
- 每个回滚段,占用一个 Rollback Segment Header Page
- 所有回滚段段头页地址[space_id, page_no],存储于Transaction System Header Page中
- 回滚段端头页管理着 Undo Slot,每个Undo Slot,存储一个Page No,指向Undo Log Header Page
- 每个更新事务,至少占用一个Undo Slot;最多占用两个Undo Slots;
- insert_undo/update_undo 分为两个队列管理:update_undo 对应 update 和 delete 操作,(过期版本数据和 undo 页)回收需要 purge;insert_undo 对应 insert 操作,在事务提交后可以直接回收。
- Undo Log Header Page
- 和 Undo Slot 对应
- 每一个 Undo Log Header Page 保存一种事务类型(update_undo/insert_undo)的 undo 记录
- 每个更新事务,至少占用一个 Undo Log Header Page,最多占用两个
- 在同一时刻,只能被一个事务使用,但是一个 Undo Log Header Page 上可以有多个事务的 undo log
- 存储内容:头结构,undo 记录
- 头结构包含:事务类型、事务状态、事务ID、下一个Undo Log Header位置等
- undo 记录包含:实际的 undo 数据,前一条/后一条 undo 记录的页面内偏移(事务级别)
- 只记录一个活跃的事务的 undo,其他 undo 对应的事务都是提交的事务或者回滚的事务
- Normal Undo Page
- 实际存储Undo记录的页面类型之一
- 存储内容:头结构,undo 记录
- 头结构包含:事务类型
- undo 记录包含:实际的 undo 数据,前一条/后一条 undo 记录的页面内偏移(事务级别)
- Normal Undo Page,通过Undo Log Header Page上TRX_UNDO_SEG_HDR结构中的双 向链表,链接起来
- 若事务Undo较小,则可能不会产生Normal Undo Page (只有Undo Log Header Page)
- Normal Undo Page与Undo Log Header Page的区别
- Undo Slot指向的是Undo Log Header Page,而非Normal Undo Page
- Normal Undo Page不包含TRX_UNDO_SEG_HDR(段头)与undo log header(日志头)
- 每个更新事务,至少会使用一个Undo Log Header Page,但是不一定会产生Normal Undo Page
- Undo Log Header Page可以被多个事务使用(串行使用),但是Normal Undo Page只属于一个事务
- 事务所用数据结构中与 Undo 相关的项
- undo_no,标识事务写的Undo记录数量,递增;
- last_sql_stat_start,事务上一条成功执行的语句写的最后一条Undo记录的undo_no ;
- rseg,事务所使用的rollback segment
- insert_undo,指向Rollback Segment Header Page中的一个Undo Slot;
- update_undo ,指向Rollback Segment Header Page中的一个Undo Slot;
- purge
- Innodb 中Delete Mark操作不会立即删除数据,只是先做了标记,而后通过 purge 操作删除数据
- 根据Undo日志,回收聚簇索引/二级索引上的被标记为删除(DEL_BIT = 1),并且不会被当前活 跃事务及新事务看到的过期版本记录;
- 选择系统中最老的提交事务(所有Rollback Segment中最老提交事务)
- 正向遍历事务的Update_Undo记录,删除聚簇/二级索引上对应的DEL_BIT=1的项(产出可能产生合并等操作)
- 在开始尝试purge前,purge线程会先克隆一个最老的活跃视图(trx_sys->mvcc->clone_oldest_view),所有在readview开启之前提交的事务所做的事务变更都是可以清理的。
事务 Crash 后恢复
- Checkpoint LSN,存储于每个日志组,第一个日志文件的LOG FILE Header内
- 从 Checkpoint LSN 开始,进行 redo
- 第一遍是遍历 (page_no, redo_no) 的 hash 表
- 第二遍是按照页并行重做,因为不同的页是独立的,可以并行,同一个页内的 redo 操作是按照顺序执行的
- redo 完成之后,进行 undo
- 根据回滚段的数据,重建事务
- 把所有没有 commit 完成的事务回滚掉
- 大事务的Rollback,会持续到Crash Recovery结束,MySQL提供服务之后
MVCC
- InnoDB 是行级多版本,快照读需要将记录回滚到可见版本(课件版本通过设定的隔离级别,以及比较事务 ID 确定);
- InnoDB 的聚簇索引记录,新增了两个系统字段 [DB_TRX_ID, ROLLBACK_PTR];
- ROLLBACK_PTR 指向记录的 Undo,根据 Undo 回滚记录到可见版本;
- 如果回滚段类型是INSERT,就完全没有必要去看Undo日志了,因为一个未提交事务的新插入记录,对其他事务而言总是不可见的。
Innodb 事务相关概念拓展
- Mini-TRansaction(MTR) ,Innodb 内部写 Log Buffer 时,为保证原子性,使用的微事务,如下图所示
- Log Buffer,所有事务的 redo log 都会先写到 Log Buffer,相关的重要指针介绍:
- flushed_to_disk_lsn: 此指针之前的日志已经flush到磁盘
- written_to_all_lsn: 此指针之前的日志已经写文件,但是未flush
- write_lsn/current_flush_lsn: 此指针之前的日志,正在写文件
- buf free: log buffer的空闲起始位置
- LSN
- Log Sequence Number,日志序列号
- LSN 递增产生
- LSN 可唯一标识一条 redo 日志(一条 redo 一个 LSN)
- LSN 有重要的意义: Checkpoint LSN, 标识数据库崩溃恢复的 redo 起点
- LSN 与日志文件位置,一一对应
- Log Write (写 redo 磁盘文件)触发条件
- 事务提交/回滚时 (参数:innodb_flush_log_at_trx_commit 打开时)
- Log Buffer 的 log free 指针超过 max_buf_free (Log Buffer 在循环利用)
- 后台线程,1S检查一次
参考文档
http://hedengcheng.com/?p=489
https://v.youku.com/v_show/id_XNDgwMTM4OTM2.html
https://draveness.me/mysql-transaction
https://juejin.im/post/5b977e56e51d450e9c553cef
https://blog.csdn.net/w_linux/article/details/79666086
https://blog.csdn.net/H12KJGJ/article/details/65937405
http://mysql.taobao.org/monthly/2015/04/01/