MySQL 事务的实现

Updated on with 0 views and 0 comments

事务隔离性由锁来实现(详见 MySQL 事务与锁),原子性、一致性和持久性通过数据库的 redo log 和 undo log 来完成。

redo

  • 基本概念
    • redo log 是 InnoDB 存储引擎层的日志,用于记录事务操作的变化,保证事务的原子性和持久性
    • 记录的是数据修改之后的值,不管事务是否提交都会记录下来
    • 由两部分组成:
      • 内存中的重做日志缓冲(redo log buffer),是易失的
      • 重做日志文件(redo log file),是持久的
    • redo log 日志的大小是固定的,即记录满了以后就从头循环写
  • 刷盘策略
    1. 发出 commit 动作时
      • 参数 innodb_flush_log_at_trx_commit 用来控制重做日志刷新到磁盘的策略
      • 默认值为 1,表示事务提交时必须调用一次 fsync 操作
      • 值为 0,表示事务提交时不进行写入重做日志操作,这个操作仅在 master thread 中完成,而在 master thread 中每 n 秒会进行一次重做日志文件的 fsync 操作,n 由参数 innodb_flush_log_at_timeout 控制
      • 值为 2,表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行 fsync 操作
    2. 刷日志的频率
      • 由参数 innodb_flush_log_at_timeout 值决定,默认是1秒
      • 要注意,这个刷日志频率和 commit 动作无关
    3. 当 log buffer 中有一半的内存空间已经被使用时
    4. 当有 checkpoint 时,checkpoint 在一定程度上代表了刷到磁盘时日志所处的 LSN 位置
      36e726b0f1fd4d89ac18dec78219580e.png
      • 上图展示了一组4个文件的 redo log 日志
      • checkpoint 之前表示擦除完了的,即可以进行写的,擦除之前会更新到磁盘中
      • write pos 是指写的位置
      • 当 write pos 和 checkpoint 相遇的时候表明 redo log 已经满了,这个时候数据库停止进行数据库更新语句的执行,转而进行 redo log 日志同步到磁盘中
      • LSN 是 Log Sequence Number 的缩写,其代表的是日志序列号
  • redo log 和 binlog 区别
    1. 重做日志是在 InnoDB 存储引擎层产生,而二进制日志是在 MySQL 数据库的上层产生的,并且二进制日志不仅仅针对于 InnoDB 存储引擎,MySQL 数据库中的任何存储引擎对于数据库的更改都会产生二进制日志,这样在数据库用别的存储引擎时可以达到一致性的要求
    2. 两种日志记录的内容形式不同:MySQL 数据库上层的二进制日志是一种逻辑日志,其记录的是对应的 SQL 语句;而 InnoDB 存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改
    3. 两种日志记录写入磁盘的时间点不同:二进制日志只在事务提交完成后进行一次写入,而 InnoDB 存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的
      e55a6704992f467ba1c1e1ee338f8fce.png
    4. redo log 是循环写,日志空间大小固定;binlog 是追加写,是指一份写到一定大小的时候会更换下一个文件,不会覆盖
    5. binlog 可以作为恢复数据使用,主从复制搭建,redo log 作为异常宕机或者介质故障后的数据恢复使用

undo

  • 基本概念
    • 保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的非锁定读
    • 保证事务一致性
    • redo 存放在重做日志文件中,与 redo 不同,undo 存放在数据库内部的一个特殊段(segment)中,这个段称为 undo 段(undo segment),undo 段位于共享表空间内
  • redo 和 undo 区别
    • undo 不是 redo 的逆过程
    • redo 和 undo 的作用都可以视为是一种恢复操作,但两者记录的内容不同
      • redo 恢复提交事务修改的页操作,且只能恢复到最后一次提交的位置,而 undo 回滚行记录到某个特定版本
      • redo 通常是物理日志,记录的是页的物理修改操作;undo是逻辑日志,根据每行记录进行记录
      • undo log 的产生会伴随着 redo log 的产生,这是因为 undo log 也需要持久性的保护
  • undo log 格式
    • insert undo log
      • insert undo log 是指在 insert 操作中产生的 undo log
      • 因为 insert 操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该 undo log 可以在事务提交后直接删除,不需要进行purge操作
    • update undo log
      • update undo log 记录的是对 delete 和 update 操作产生的undo log
      • 该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除
      • 提交时放入 undo log 链表,等待 purge 线程进行最后的删除

purge

  • 基本概念
    • purge 用于最终完成 delete 和 update 操作
    • 这样设计是因为 InnoDB 存储引擎支持 MVCC,如果这时其他事物可能正在引用这行记录,那么 InnoDB 存储引擎需要保存记录之前的版本,所以记录不能在事务提交时立即进行处理
    • 而是否可以删除该条记录通过 purge 来进行判断,若该行记录已不被任何其他事务引用,那么就可以进行真正的 delete 操作
    • 可见,purge 操作是清理之前的 delete 和 update 操作,将上述操作最终完成,而实际执行的操作为 delete 操作,清理之前行记录的版本

group commit

  • 基本概念
    • 将多个事务的重做日志通过一次 fsync 刷新到磁盘,这样就大大地减少了磁盘的压力,从而提高了数据库的整体性能
    • 起初,group commit 是针对重做日志的
    • 对于写入或者更新频繁的操作,group commit 的效果尤为明显
  • group commit 失效问题
    • 导致这个问题的原因是在开启二进制日志后,为了保证存储引擎层中的事务和 binlog 的一致性,二者之间使用了两阶段事务,其步骤如下:
      874bc78399554c9cb023d05ba4fadd3e.png
      1. Prepare 阶段
        • 此时 SQL 已经成功执行,并生成事务 ID(xid)信息及 redo 和 undo 的内存日志
        • 此阶段 InnoDB 会写事务的 redo log,但要注意的是,此时 redo log 只是记录了事务的所有操作日志,并没有记录提交(commit)日志,因此事务此时的状态为 Prepare
        • 此阶段对binlog不会有任何操作
      2. Commit 阶段, 这个阶段又分成两个步骤:
        • 第一步写 binlog,先调用 write() 将 binlog 内存日志数据写入文件系统缓存,再调用 fsync() 将 binlog 文件系统缓存日志数据永久写入磁盘(这个 fsync 由参数 sync_binlog 控制)
        • 第二步完成事务的提交(commit),此时在 redo log 中记录此事务的提交日志,增加 commit 标签(这个 fsync 由参数 innodb_flush_log_at_trx_commit 控制)
    • 注意:在这个过程中是以第二阶段中 binlog 的写入成功与否作为事务是否成功提交的标志,此时的崩溃恢复过程如下:
      1. 如果数据库在记录此事务的 binlog 之前和过程中发生 crash
        • 数据库在恢复后认为此事务并没有成功提交,则会回滚此事务的操作
        • 与此同时,因为在 binlog 中也没有此事务的记录,所以从库也不会有此事务的数据修改
      2. 如果数据库在记录此事务的 binlog 之后发生 crash
        • 此时,即使是 redo log 中还没有记录此事务的 commit 标签,数据库在恢复后也会认为此事务提交成功,因为在上述两阶段过程中,binlog 写入成功就认为事务成功提交了
        • 它会扫描最后一个 binlog 文件,并提取其中的事务 ID(xid),InnoDB 会将那些状态为 Prepare 的事务(redo log 没有记录 commit 标签)的 xid 和 binlog 中提取的 xid 做比较,如果在 binlog 中存在,则提交该事务,否则回滚该事务
        • 这也就是说,binlog 中记录的事务,在恢复时都会被认为是已提交事务,会在 redo log 中重新写入 commit 标志,并完成此事务的重做(主库中有此事务的数据修改)
        • 与此同时,因为在 binlog 中已经有了此事务的记录,所有从库也会有此事务的数据修改
    • 为什么需要保证 MySQL 数据库上层二进制日志的写入顺序和 InnoDB 层的事务提交顺序一致呢?这时因为备份及恢复的需要,例如通过工具 xtrabackup 或者 ibbackup 进行备份,并用来建立 replication,如图所示:
      5292a77a64b84a81817cf0f0eb5d103b.png
      • 事务按照 T1、T2、T3顺序写入二进制日志,调用 fsync 进行一次 group commit 将日志文件永久写入磁盘,但是存储引擎提交顺序为 T2、T3、T1
      • 当 T2 和 T3 提交事务后,若通过在线备份进行数据库恢复来重新建立 replication,会发生主备数据不一致(搭建 slave 时,change master to 的日志偏移量记录在 T3 的事务位置之后)
      • 因为在 InnoDB 存储引擎层会检测事务 T3 在上下两层都完成了提交,所以认为不需要再进行恢复了
    • 为了保证 MySQL 数据库上层 binlog 的写入顺序和 InnoDB 层的事务提交顺序一致,MySQL数据库内部使用了 prepare_commit_mutex 这个锁
    • 在启用 prepare_commit_mutex 这个锁之后,只有当上一个事务 commit 后释放锁,下一个事务才可以进行 prepare 操作,从而导致了group commit 失效
  • 解决方案
    05ce1bdefa8c49038819ac2c90a5cfe0.png
    • 在 MySQL 数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为 leader,其他事务称为 follower,leader 控制着 follower 的行为,分为以下三个阶段:
      • Flush 阶段,将每个事务的二进制日志写入内存中
      • Sync 阶段,将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次 fsync 操作就完成了二进制日志的写入,这就是 BLGC(Binary Log Group Commit)
      • Commit 阶段,leader 根据顺序调用存储引擎层事务的提交,InnoDB 存储引擎本就支持 group commit,因此修复了原先由于锁 prepare_commit_mutex 导致 group commit 失效的问题
    • 采用这种方案,不但 MySQL 数据库上层二进制日志写入是 group commit 的,InnoDB 存储引擎层也是 group commit 的
  • 注意事项
    • 当引入 group commit 后,sync_binlog 的含义就变了,假定设为1000,表示的不是1000个事务后做一次 fsync,而是1000个事务组
      • 也就是说,当设置 sync_binlog=1,binlog 还未落盘,此时系统 crash,会丢失对应的最后一个事务组
      • 如果这个事务组内有10个事务,那么这10个事务都会丢失
    • 参数 binlog_max_flush_queue_time 用来控制 Flush 阶段中等待的时间,即使之前的一组事务完成提交,当前一组的事务也不马上进人 Sync 阶段,而是至少需要等待一段时间
      • 这样做的好处是 group commit 的事务数量更多,然而这也可能会导致事务的响应时间变慢
      • 该参数的默认值为0,且推荐设置依然为0,除非用户的 MySQL 数据库系统中有着大量的连接(如100个连接),并且不断地在进行事务的写人或更新操作

附录

509e2a5159f64ea68d991621650a182a.png


标题:MySQL 事务的实现
作者:yanghao
地址:http://solo.fancydigital.com.cn/articles/2021/09/28/1632813336419.html