mysql支持undo操作吗?Undo日志到MVCC实现原理
mysql支持undo操作吗?Undo日志到MVCC实现原理存在问题对于insert操作,undo 日志记录新数据的 PK(ROW_ID),回滚时直接删除对于delete/update操作,undo 日志记录旧数据,回滚时直接恢复现需要将 id=1 的 name 修改为 kawaii,流程如下:1、事务开始2、将 id=1 的行数据记录到 undo log buffer3、修改 id=1 的行数据 name 为 kawaii4、undo log buffer 写入磁盘为 undo log file5、将数据写入磁盘6、事务提交步骤 1 - 3 都是在内存中完成,第 4 步之前包括第 4 步如果出现问题,undo log 与数据都未写入磁盘,可以直接回滚,第 4 步之后出现问题,此时 undo log 已写入磁盘,利用 undo log 进行回滚。
文章大纲- Undo日志
- Redo日志
- MVCC
关系型数据需要实现事务的 ACID 特性,其中一点就是事务的原子性,Mysql 就是通过 Undo 日志就来实现的。
数据库处理数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘。
在事务处理过程中,操作数据之前会先将数据缓存至 Undo 日志,然后进行数据的修改。当事务回滚时,或者数据库奔溃时,系统可以利用 Undo 日志中的备份将数据恢复到事务开始之前的状态,撤销未提交事务对数据库产生的影响。
具体流程假设有个用户表,字段有 id、name。
现需要将 id=1 的 name 修改为 kawaii,流程如下:
1、事务开始
2、将 id=1 的行数据记录到 undo log buffer
3、修改 id=1 的行数据 name 为 kawaii
4、undo log buffer 写入磁盘为 undo log file
5、将数据写入磁盘
6、事务提交
步骤 1 - 3 都是在内存中完成,第 4 步之前包括第 4 步如果出现问题,undo log 与数据都未写入磁盘,可以直接回滚,第 4 步之后出现问题,此时 undo log 已写入磁盘,利用 undo log 进行回滚。
对于insert操作,undo 日志记录新数据的 PK(ROW_ID),回滚时直接删除
对于delete/update操作,undo 日志记录旧数据,回滚时直接恢复
存在问题
事务提交时需要将内存中的数据同步写入磁盘,数据写入磁盘属于随机IO,性能较差,会极大影响数据库的吞吐量。
优化方案:将修改行为先写到 Redo 日志(顺序写),再定期将数据刷到磁盘上,这样能极大提高性能。
Redo 日志Undo 日志存储的是历史数据快照,Redo 日志则存储最新数据。
具体流程还是以上文中的操作为例:
1、事务开始
2、将 id=1 的行数据记录到 undo log buffer
3、修改 id=1 的行数据 name 为 kawaii
4、记录修改日志到 redo log buffer
5、undo log buffer 写入磁盘为 undo log file
6、redo log buffer 写入磁盘为 redo log file
7、事务提交
都是写磁盘,写 Redo 日志文件与直接写数据库文件有什么区别?
写 Redo 日志文件是顺序IO,而写数据库文件是随机IO,性能差别大。
Redo 日志文件写入成功后,数据库会另起线程将 Redo 日志写入数据库数据文件,实现持久化。
MVCC(Multi-Version Concurrency Control)即多版本并发控制,Mysql、Oracle、PostgreSQL等数据库都实现了MVCC,但各自实现的机制不尽相同,因为MVCC没有统一的实现标准。
事务隔离级别事务有四个隔离级别
- 脏读:事务中的未提交的修改对其它事务是可见的。
如下表所示,事务B可以读取到事务A未提交的数据。
- 重复读:事务中多次读取同一数据,结果不一致。
如下表所示,事务B两次读取之间事务A修改了数据并提交事务,事务B的第二次读取与第一次读取结果有变化。
- 幻读:一个事务在读取某个范围内的记录时,另一个事务在该范围插入的新记录,之前事务再次读取该范围记录时,会产生幻行。
如下表所示,事务B两次读取之间事务A插入了一条数据并提交事务,事务B第二次读取 id < 5 的范围数据时,数量增加了。
InnoDB 存储引擎默认的事务隔离级别为 RR (可重复读)
那么 InnoDB 如何实现可重复读?加行级锁是肯定可以实现的,但如果对读操作也加行级锁,将严重影响数据库的并发性能,而MVCC 就是这个问题的解决方案。
MVCC 是一种用来解决读-写冲突的无锁并发控制,通过对行数据的多版本控制,避免了读操作时的加锁操作,因此开销更低,大大提高数据库系统的并发性能。
InnoDB 就是通过 行级锁 MVCC 共同实现事务隔离,正常读的时候不加锁,写的时候对数据行加排它锁。
MVCC 只能在 Read Committed 和 Repeatable Read 两个隔离级别下工作。
MVCC实现原理存储结构InnoDB 支持聚簇索引,默认设置主键列为聚簇索引,如果表中无主键,则会选择一个唯一的非空索引作为聚簇索引,如果也没有,则会隐式定义一个主键作为聚簇索引。
InnoDB 的数据行存储在聚簇索引上,结构如下:
可以看到索引上不仅存储了索引列、行数据,还包含了两个隐藏字段:
- DB_TRX_ID(6字节):表示最近一次修改(insert | update)的事务ID。
- DB_ROLL_PTR(7字节):回滚指针,指向这条记录的上一个版本(存储于 roolback segment 里的 update undo log)
还有一个删除 flag 隐藏字段,delete 操作被认为是一个 update 操作,只是修改了该删除标记位,而不是物理删除。
以上文中的表结构为例:
第一步:事务1,插入一条记录
第二步:事务2,修改 name 为 kawaii,流程如下
1、事务开始,对数据行加排它锁
2、将该行数据记录到 undo log,事务ID 为 1 的数据行
3、修改该行数据 name 为 kawaii,DB_TRX_ID 修改为 2,DB_ROLL_PTR 指向 步骤 2 中记录的 undo log
4、提交事务,释放排它锁
当前最新数据如下,其中 0x23636355 指向 undo log 中 DB_TRX_ID 为 1 的地址
第三步:事务3,修改 name 为 unknown,流程如下
1、事务开始,对数据行加排它锁
2、将该行数据记录到 undo log,事务ID 为 2 的数据行
3、修改该行数据 name 为 unknown,DB_TRX_ID 修改为 3,DB_ROLL_PTR 指向 步骤 2 中记录的 undo log
4、提交事务,释放排它锁
当前最新数据如下,其中 0x65461234 指向 undo log 中 DB_TRX_ID 为 2 的地址
可以看出,通过 DB_ROLL_PTR 指针,同一行数据的多个版本形成了一个单向链表,链表中除第一条记录,都存储在 undo log。
ReadView 结构通过 undo log 实现数据的多版本存储,接下来需要处理的就是数据读取的问题,该读取哪个版本的数据?ReadView 就是用来做可见性判断的。
ReadView 结构有几个重要的变量
- creator_trx_id:当前事务的ID,每个事务都会拥有一个ID,是一个递增的编号。
- trx_ids:Read View创建时其他未提交的活跃事务ID列表。
- low_limit_id:目前出现过的最大的事务ID 1,即下一个将被分配的事务ID。
- up_limit_id:活跃事务列表 trx_ids 中最小的事务ID,如果 trx_ids为空,则 up_limit_id 为 low_limit_id。
InnoDB 会在事务执行第一个 select 语句的时候创建 ReadView,读取某行记录的时候,根据该行的DB_TRX_ID 与 ReadView 进行可见性分析,判断读取哪个版本的数据。
具体的可见性比较算法如下:
- 判断 DB_TRX_ID < up_limit_id,如果成立,表明 最新修改该行的事务 在 当前事务 创建 ReadView 之前就已经提交,所以 DB_TRX_ID 版本对 当前事务 是可见的,否则进入下一个判断
- 判断 DB_TRX_ID >= low_limit_id,如果成立,表明 最新修改该行的事务 在 当前事务 创建 ReadView 之后才修改该行,所以DB_TRX_ID 版本对 当前事务 是不可见的
- 判断 DB_TRX_ID 是否在活跃事务列表 trx_ids 中如果在,表明在 当前事务 创建 ReadView 时,最新修改该行的事务 还在活跃中,尚未提交,所以 DB_TRX_ID 版本对 当前事务 是不可见的。如果不在,表明在 当前事务 创建 ReadView 之前,最新修改该行的事务 就已经提交,所以 DB_TRX_ID 版本对 当前事务 是可见的。
- DB_TRX_ID 版本对 当前事务 不可见,则根据 DB_ROLL_PTR 取出上一个版本数据,使用其 DB_TRX_ID 重新判断一次。
看完规则后,也许脑瓜子是嗡嗡的,那么接下来做一个模拟分析
事务2 执行快照读时,数据库为该行数据创建一个 ReadView,此时事务1、3处于活跃状态,事务4已提交,那么 ReadView 中各个变量的值如下:
trx_ids = [1 3]
low_limit_id = 4 1 = 5
up_limit_id = 1
123
事务2读取最新记录,该记录的 DB_TRX_ID = 4,根据上文中的规则
- 判断 DB_TRX_ID < up_limit_id,不成立,下一步
- 判断 DB_TRX_ID >= low_limit_id,也不成立,下一步
- 判断 DB_TRX_ID 是否在 trx_ids 集合中,结果为否,符合可见性条件。
所以事务4提交的最新结果对事务2是可见的。
再来一个更复杂的模拟分析,原始金额为 500,DB_TRX_ID 为 100
前文中给出的示例都是基于 RR 隔离级别,下文给出一个 RR 隔离级别的简单示例,方便与 RC 隔离级别进行比较。
RR 隔离级别
RC 隔离级别
都是使用 MVCC 进行实现,同样的操作需要得到不同的结果,区别就在于创建 ReadView 的时机,RR 隔离级别只在第一个 select 语句时创建,而 RC 隔离级别则是每一个 select 语句都创建。