MySQL InnoDB中一个update语句从执行到提交的全过程(2)

接上文MySQL InnoDB中一个update语句从执行到提交的全过程(1)-CSDN博客

目录

四、校验锁和加锁

五、修改数据和生成日志

1. 写入内存中的数据页(Buffer Pool)

2. 写入撤销日志(Undo Log)

3. 写入重做日志(Redo Log)

总结


四、校验锁和加锁

在上文,我们找到需要update的这条记录之后,接下来就准备对数据进行修改了, 但在修改之前,我们需要确认对这条数据的修改是否是安全的。

结合事务的隔离级别,我们需要对这行数据进行锁校验并加上我们要求的锁。

我们这条update语句,首先会在mysql服务器层获取表的元数据读锁,然后在InnoDB存储引擎层上表的意向排他锁,最后上这条记录的行级X排他锁。

这个步骤有两个地方可能会被阻塞,一个是获取表的元数据读锁,另一个是获取行级锁。因为mysql有元数据锁,所以表级锁在innodb中基本没有实际效果,我们就不考虑表级锁了。

(1)如果正好有其他的DDL操作正在修改当前表,比如增加索引,修改表字段等等,那么这条update语句获取元数据锁的过程会被阻塞,因为其他的DDL操作会持有该表的元数据锁。

(2)在innodb存储引擎中,对一条记录加锁的本质就是在内存中创建一个锁结构与之相关联,包含锁类型、锁模式、锁定对象、事务ID、锁状态、索引和锁链等信息,用于管理并发事务时的数据一致性和安全性。

加锁的锁结构主要包含以下内容:

  1. 锁类型(Lock Type)
    锁的类型决定了该锁是共享锁(S 锁,Shared Lock)还是排他锁(X 锁,Exclusive Lock)。

    • 共享锁 (S 锁):允许其他事务对相同的数据也加共享锁,但不允许加排他锁。
    • 排他锁 (X 锁):不允许其他事务对相同的数据加任何锁(无论是共享锁还是排他锁)。
  2. 锁模式(Lock Mode)
    锁模式指示了锁的力度,常见的锁模式包括:

    • Record Lock:对单个记录的锁。
    • Gap Lock:对记录之间的间隙加锁,防止其他事务在锁定的间隙插入新记录。
    • Next-Key Lock:结合了 Record Lock 和 Gap Lock,锁定一条记录以及记录前的间隙。
  3. 锁定对象(Lock Object)
    锁定的具体对象可以是表(Table)、页(Page)、记录(Record)。

    • 表锁:锁定整个表,适用于一些 DDL 操作。
    • 页锁:在存储页层面上加锁,通常不会直接使用页锁,而是用记录锁或间隙锁。
    • 记录锁:锁定具体的行记录。
  4. 事务ID(Transaction ID)
    锁结构中包含了加锁事务的事务ID(Transaction ID),这用于标识哪个事务拥有该锁。

  5. 锁状态(Lock Status)
    锁的状态可以是等待中或已经获得。锁结构需要记录当前锁的状态,以决定事务是否需要等待或可以立即执行操作。

  6. 索引(Index)
    当使用行级锁时,InnoDB 通常依赖于索引进行加锁。锁结构中包含了加锁的索引信息,因为行级锁是在索引上的。

  7. 锁链(Lock Chain)
    锁结构中可能还包括锁链信息,用于处理多个事务之间的锁依赖和等待链,特别是在死锁检测中非常重要。

获取表的元数据读锁和表级X锁之后,就可以对查找出来的这条记录进行行锁校验。

如果这条记录上面有其他事务上的锁,那么当前update语句就要阻塞。这时当前update语句就会等待,直到锁释放之后会有一个线程来唤醒它才能继续后续修改操作。

如果这条记录上面没有其他事务上的锁,那么当前update语句开始生成一个锁内存结构(是一个用来管理和跟踪锁的内存数据结构),来记录上锁的信息。这个内存结构用于维护锁的状态,并确保在并发情况下数据的一致性和完整性。

假设我们这个例子中的隔离级别是MySQL默认的可重复读,那么当前update操作就会对这行数据加上一个行级的X锁,另一种常用的读已提交隔离级别在这里的表现也是一样的。

五、修改数据和生成日志

加上锁之后,就可以安全地对我们前面查到的数据进行修改操作了。

当执行修改操作时,InnoDB 存储引擎会写三部分内容:修改内存中的数据页、记录重做日志、记录撤销日志这三个步骤来确保数据的可靠性与一致性。通过这种方式,InnoDB 不仅能够支持高效的并发控制,还能在系统崩溃或事务回滚时进行数据恢复,从而保障数据的完整性。

1. 写入内存中的数据页(Buffer Pool)

Buffer Pool 是 InnoDB 的一个内存结构,缓存了从磁盘上读取的表和索引数据。当我们对数据进行修改时,首先修改的是内存中的数据页。

  • 如果修改前后这行数据的大小完全没变,那么直接就地更新
  • 如果任何字段的大小发生了变化,那么把旧记录放到页的垃圾链表中,然后插入修改后的新记录
  • 存储在数据页中的每行数据会有trx_id和roll_pointer两个隐藏字段
    • 每次增删改数据,都需把这行数据的tx_id修改成当前事务的trx_id
    • 修改roll_pointer:指向前序的undolog的指针,构成版本链

这些修改会暂时保存在内存中,直到内存页被刷写回磁盘。这样可以减少对磁盘的频繁 I/O 操作,提高数据库性能。

此时,我们对这个页上的PageXLatch也就可以释放了,别的线程可以正常访问这个页了。但对于我们刚修改完的这条记录上的行级X锁依旧存在,在非RU(读未提交)隔离级别下,都是读不到的。

2. 写入撤销日志(Undo Log)

撤销日志保存的是修改操作的前镜像(即修改前的数据),用于在事务回滚时恢复数据到原始状态。

根据操作的不同,Undo Log 主要分为两种类型:Insert Undo LogUpdate Undo Log。它们分别用于记录 INSERTUPDATE/DELETE 操作的撤销信息。下面分别介绍这两种 Undo Log 的作用和特点:

1)Insert Undo Log 记录的是 INSERT 操作的撤销信息。

  • 当一个事务插入一条新记录时,Insert Undo Log 会记录一条删除该记录的撤销信息。这样,如果事务需要回滚,InnoDB 可以利用 Insert Undo Log 删除该记录,从而恢复到插入操作前的状态。
  • 特点

    • Insert Undo Log 仅在事务回滚时使用,因为一旦事务提交,插入的记录就成为了数据库的永久部分,不需要再进行撤销操作。
    • 由于 Insert Undo Log 不涉及 MVCC,所以在事务提交后,相关的 Insert Undo Log 可以很快被清理和回收。
    • Insert Undo Log 通常比 Update Undo Log 体积小,因为它只需要记录如何删除一条记录的信息。

2)Update Undo Log 记录的是 UPDATEDELETE 操作的撤销信息。

  • 当一个事务对记录进行更新或删除操作时,Update Undo Log 会记录该记录的修改前的旧值。这样,如果事务需要回滚,InnoDB 可以利用 Update Undo Log 将记录恢复到更新或删除操作前的状态。

  • 特点

    • 除了支持回滚外,还用于 MVCC。通过保存旧版本的数据,InnoDB 能够为并发事务提供一致性读,允许事务在不同时间点看到数据的不同快照。
      • 查询语句在查询前会生成一个ReadView,和查找到数据的trx_id进行比较,不符合条件则通过roll_pointer向前追溯,直至找到符合条件的版本
    • Update Undo Log 会在事务提交后保留一段时间,直到不再需要支持 MVCC 时才被清理。这个清理过程通常由 InnoDB 的后台线程负责。
    • 由于需要保存数据的旧版本,Update Undo Log 的体积通常比 Insert Undo Log 大,特别是在频繁更新或删除操作时。

这里, 我们生成的undo log会写到undo log buffer中,等待后续的落盘。

3. 写入重做日志(Redo Log)

重做日志记录了对数据的每一个修改操作,用于在崩溃恢复时重演这些操作,确保数据库的持久性(即数据不会因崩溃丢失)。

Redo Log 以顺序方式写入磁盘,写入性能非常高。每次事务提交时,都会强制将相应的重做日志刷写到磁盘上,以确保事务的持久性(即所谓的 WAL(Write-Ahead Logging)策略)。

哪怕对于我们这样一个非常简单的update语句,产生的redolpg大概率也不只一条,因为我们对数据页的改动不只一处。

  1. 因为这行数据的字符数发生了变化,而是要删除旧记录,再插入修改后的新记录
  2. 数据页中的记录会按照索己顺序连成一个单向链表,在上一步产生的变化之后,我们还要对上一条记录的next_record属性进行修改
  3. 数据页的Page Directory和PageHeader的一些内容可能产生变化
  4. 导致节点分裂与合并的情况

这个修改至少造成了页中这么多位置的修改,所有这些修改都要被redlog记录下来。简单粗暴的做法就是直接把整个页改动的内容都记入redolog,但这样显然redolog就太大了。这就是为什么Redo Log 中的日志条目种类繁多的原因,目前的 Redo Log Type 类型多达 65 种,分别针对不同的修改与记录。

总之,我们这个update语句会生成很多个小的redo log(称为一组,MTR)。一个MTR可以包含一组redo log,无论是写入还是恢复时,都要保证这组redo log的原子性。

redolog buffer的大体结构:

  • 把redo log存入log buffer中,以作写入磁盘的缓冲。log buffer是一块连续的内存空间,由一个个512B的block组成。

    生成的redolog会找到log buffer的最新一个redolog block顺序写入。

  • redolog还有一个全局递增的日志序列号LSN ,用来标记redolog buffer的位置,因为一个MTR的redolog是作为一组写入redologbuffer的,所以LSN也是以MTR为单位增长的。

此时,对于整个SQL执行过程中产生的任何信息包括修改后的数据页、redolog、undo.log还有后面会介绍的binlog,目前都仅存在于内存中,这个时候如果崩了,这些信息都会丢失,相当于这条update语句操作从未执行过,也不用考虑怎么把它恢复了。所以sql执行过程中,是没必要要求redolog落盘的。

但是log buffer毕竟只有16MB,还是有很多原因可能导致redolog落盘的:

  • 事务提交时
  • log buffer空间不足(低于50%)时
  • 后台线程周期性刷log buffer
  • MySQL服务正常关闭时
  • 做checkpoint时

一旦未提交事务的redolog因为上面这些原因落盘了,反而比较麻烦。因为如果这时宕机恢复,对这些redolog进行恢复会造成未提交事务的脏页的恢复,对这种情况要进行一些特殊处理。

此外还有,undolog在生成的过程中也会产生对应的redolog,毕竟undolog也是我们要保证持久化的重要内容。

总结

我们到底在存储引擎中写了哪些东西?

  • 数据页:增删改数据的本体,用作读取;
  • undo log:记录旧值,用作回滚和MVCC;
  • redo log: Write Ahead Log,保证持久性和数据页、undo log的安全。

恢复的过程:从checkpoint_Isn位置开始读取redo log,来恢复脏页和undo log,然后通过undolog把所有未提交事务的脏页进行回滚。

由此,我们就能保证所有已提交的事务都没丢失数据,且所有未提交事务都回滚了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值