聊一聊 MySQL 事务

InnoDB Architecture

InnoDB 架构

什么是事务(Transaction)

事务(Transaction)是访问和更新数据库的程序执行单元;是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位。
事务是逻辑上的一组操作,要么都执行,要么都不执行。

MySQL 中服务器层是不管理事务的,事务是由存储引擎实现的,下文中统一表示为 InnoDB

事务的特性

理论上来说,事务有着极其严格的定义,它必须同时满足四个特性:

  • 原子性(Atomicity)
    事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
  • 一致性(consistency)
    事务应确保数据库的状态从一个一致状态转变为另一个一致状态
  • 隔离型(isolation)
    多个事务并发执行时,一个事务的执行不应影响其他事务的执行
  • 持久性(durability)
    已被提交的事务对数据库的修改应该永久保存在数据库中

接下来将逐一解释这四个特性:

事务的实现

事务是如何实现 ACID 的

事务的实现是通过redo log && undo log, 以及 来实现的。

redo log 实现持久化和原子性,undo log 实现一致性,锁实现事务的隔离性。
redo log 是恢复提交事务修改的页操作,undo log 是回滚行记录到特定的版本。二者记录的内容也不同,redo log 是物理日志,记录页的物理修改操作,undo log
是逻辑日志,根据每行记录进行记录。

原子性

原子性表示一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;
如果事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

原子性的实现 - undo log

首先,介绍一下各种日志,MySQL 的日志类型有很多种,例如二进制日志、错误日志、查询日志、慢查询日志等
在 InnoDB 存储引擎还提供了两种事务日志:

  • redo log (重做日志)
  • undo log(回滚日志)
    其中 redo log 用于保证事务持久性undo log 则是事务原子性和隔离型实现的基础

一致性

一致性是指事务将数据库从一种一致性状态转变为下一种一致性状态。在事务开始之前和之后,数据库的完整性约束没有被破坏。

一致性事务的状态

因为事务是原子性的,所以从整体上来看的话,事务是密不可分的一个整体那么就可以将事务分状态看:

  • Active(执行中)
  • Commited(执行成功,已提交)
  • Failed(执行失败)

但是如果放大来看的话,事务也分为多个中间态。

  • Active: 事务的初始状态,表示事务正在执行;
  • Partially Commited: 在最后一条语句执行之后;
  • Failed: 发现事务无法正常执行之后;
  • Aborted: 事务被回滚并且数据库恢复到了事务进行之前的状态之后;
  • Commited: 成功执行整个事务;

redo log

通过以上的介绍,我们知道 redo log 是实现事务的原子性持久性。redo log 由两部分组成:

  • 内存中的日志缓存(redo log buffer)
  • 重做日志文件(redo log file)

redo log 的更新流程

以一次 update 例子:

  • update 操作
  • 先将原始数据从磁盘中读取到内存,修改内存中的数据
  • 生成一条重做日志写入 redo log buffer, 记录数据被修改后的值
  • 当事务提交时,需要将 redo log buffer 中的内容刷新到 redo log file
  • 事务提交后,将内存中修改的数据写入磁盘中

为了确保每次日志都写入重做日志文件,InnoDB 存储引擎会调用一次 fsync 操作

redo log 存储格式内容

首先比较一下 binlog 二进制日志,binlog 主要是主从复制和进行 POINT-IN-TIME 的恢复

binlog 只有在事务提交的时候才会写入,且是数据库的上层中产生的。redo log 是 Innodb 引擎层产生的。

binlog 日志是每次事务才会写入,所以每个事务只会有一条日志,记录的是 SQL 语句

redo log 是事务开始就会写入,*T1 表示事务提交。记录的是物理格式日志,即每个页的修改

redo log 默认是以 block(块)的方式为单位进行存储,每个块是 512 个字节。不同的数据库引擎有对应的重做日志格式,Innodb 的存储管理是基于页的,所以其重做日志也是基于页的

  • redo_log_type 重做日志类型
  • space 表空间的 ID
  • page_no 页的偏移量
  • redo_log_body 存储内容

当我们执行插入语句时,日志内容为:

1
2
3
4
INSERT INTO user values (1,2,3,43);

page(2,3), offset 32, value 1,2,3,43 # 主键索引
page(2,4), offset 64, value 2 # 辅助索引

那么 redo log 为什么可以实现事务的原子性和持久性呢?

  • 原子性,redo log 记录了事务期间操作的物理日志,事务提交之前,并没有写入磁盘,保存在内存中,如果事务失败,数据库磁盘将不会发生改变,回滚掉内存部分的数据即可
  • 持久性,redo log 会在事务提交时将日志存储到磁盘 redo log file, 保证日志的持久性

undo log

什么是 undo log

undo log 则是事务原子性隔离性实现的基础。
重做日志记录了事务的行为,可以友好地通过其对页进行重做操作。
当发生事务性操作的时候,InnoDB 就会产生一定量的 undo log。当回滚或者事务执行失败的情况发生的时候,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。
undo log 是采用段(segment)的记录方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment

由于 undo log 属于逻辑日志,不能物理地将数据库恢复成执行语句或者事务之前的样子。
所有的修改操作都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。

这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的任务就是协调对数据记录的并发访问。
对于表空间而言,并不会修改成之前的样子。

undo log 是如何恢复数据的?

undo log 属于逻辑日志,它记录的是 sql 执行相关的信息, 当用户执行 ROLLBACK 时

  • 对于每个insert,记录delete;
  • 对于每个delete,记录insert;
  • 对于每个update,记录之前的update;

通过以上的操作便可实现回滚

事务的隔离级别

事务的隔离级别是通过锁来实现

四种隔离级别分别是, 从低到高:

  • READ-UNCOMMITTED(读取未提交)
  • READ-COMMITTED(读取已提交)
  • REPEATABLE-READ(可重复读)
  • SERIALIZABLE(可串行化)
隔离级别 脏读 不可重复读 幻影读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

Mysql 默认采用的 REPEATABLE_READ 隔离级别

什么是脏读?幻读?不可重复读?

  • 脏读(Dirty Read):
    读取到了错误的数据,A 事务执行 update 操作,B 事务执行查询获取结果,此时 A 事务 RollBack 导致读取数据不正确。
  • 不可重复读(Non-repeatable read):
    在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据
  • 幻读(Phantom Read):
    一个事务的两次查询中夹杂着插入事务的操作,数据量不一致

总结

理解事务的特性,原子性,一致性,持久性,隔离性,可以在使用中,更好的定位到问题,更好的设计业务系统。

-------------THANKS FOR READING-------------