跳至主要內容

Mysql 事务

blacklad大约 8 分钟MysqlMysqlMysql 技术内幕

Mysql 事务

事务是数据库区分文件系统的重要特性之一,可以保证数据库从一种一致状态转换到另外一种状态,要么所有的修改都保存了,要么所有的修改都不保存。

1 事务定义

事务需要满足 ACID 四个特向,但是一般数据库厂商为了性能,并不会严格去满足 ACID 标准。

1.1 A 原子性

一个事务中的操作要么都做,要么都不做。即原子性指整个事务是不可分割的工作单位,只有事务中的所有数据库操作都执行成功才算整个事务执行成功。

当事务中发生错误时,已经执行成功的 sql 语句需要撤销,数据库状态应该退回到执行事务前的状态。

1.2 C 一致性

事务从一种状态转变为下一种一致的状态。在事务开始前和结束以后,事务的完整性都没有被破坏,数据依旧满足数据库的完成性约束条件,如唯一约束。

1.3 I 隔离性

要求每个读写事务的对象对其他事务的操作相互分离,即该事务提交前对其他事务都不可见。

1.4 D 持久性

事务一但提交,其结果就是永久的,即使发生宕机等故障也能将数据恢复。只能从事务本身的角度保证结果的永久性。

2 事务的实现

事务的隔离性是通过锁实现,原子性和持久性通过 redo log 实现,一致性是通过 undo log 实现。

3 redo log

redo log 由 内存中的重做日志缓冲和重做日志文件组成。InnoDB 是事务的存储引擎,通过 Force Log at Commit 机制实现事务的持久化,当事务提交时,必须先将该事务的所有日志写入到重做日志文件中持进行持久化,待事务的提交操作完成才算完成。redo log 基本上是顺序写的,在数据库运行的时候不需要进行读取操作。

为了确保写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,还需要调用一次 fsync 将内容从文件系统的缓存刷新到文件中。

3.1 同步日志刷新策略

通过配置 innodb_flush_log_at_trx_commit 可以控制重做日志刷新到文件的策略:

  1. 默认值 1 表示每次事务提交时必须调用一次 fsync 操作。
  2. 参数为 0 时表示事务提交时不执行写入重做日志操作,这个操作尽在 Master Thread 中定时执行。
  3. 参数为 2 时表示提交时将数据写入重做文件,但不执行 fync 操作,仅会将数据写入文件系统的缓冲,如果只是数据库发生宕机并不会影响事务的丢失。

3.2 重做日志块 log block

在 InnoDB 中,重做日志都是以 512 字节进行存储,都是以块的方式存储,若大于 512 字节,则进行分割成多个日志块,512 字节可以保证和磁盘扇区的大小一样,因此重做日志的写入可以保证原子性。

重做日志块包含 日志块头、日志本身、日志块尾三个部分。

日志块头中包含 日志块所在位置、日志块大小、第一个日志偏移量等信息。

日志块尾也包含日志块所在位置信息。

3.3 重做日志组

重做日志组是一个逻辑概念,一个组由多个重做日志文件组成,当一个file被写满时就会追加到下一个file。

每个redo log file前2kb保存了4个512字节大小的块,在每次写入的时候也需要更新这2KB的信息。

3.4 重做日志格式

redo_log_type 重做日志类型、space 表空间 idpage_no 页的偏移量、redo log body 重做日志内容组成。

3.5 LSN

Log Sequence Number 日志序列号,单调递增,单位是字节数,包含:重做日志写入的总量、checkpoint 的位置、页的版本等信息。

在每一个页的头部会记录该页最好刷新的 LSN 的大小,来判断当前页是否需要进行恢复操作。

存在:当前最新的 LSN、刷新到重做日志文件的 LSN、刷新到磁盘的 LSN(checkpoint)

3.6 恢复

InnoDB 存储引擎在启动时不管是否正常关闭,都会尝试恢复操作。且重做日志是物理日志,恢复速度快。

checkpoint 表示已经刷新到磁盘上的 LSN,只需要把 checkpoint 开始的日志进行恢复。

重做日志是页 的物理操作,内容大概是:

且操作是幂等的,即 f(f(x)) = f(x), 而二进制日志并不是幂等的,如 Insert 操作重复执行可能会插入重复记录。

4 undo log

undo log 用来回滚事务,存放在 undo 段中,undo 段位于共享表空间内。undo 是逻辑日志,只是逻辑的将数据库恢复到原来的样子,修改被逻辑的取消了,但数据结构和页本身可能和回滚前不一样

用来实现 MVCC,当读取一行记录时,可以通过 undo 读取之前版本的行信息,实现非锁定读取。

事务在 undo log segment 分配页并写入 undo log 的这个过程也需要写入重做日志,undo log 也需要持久性的保护。

当事务提交时,由于可能有其他事务需要通过 undo log 读取行记录之前的版本,所以并不能马上删除 undo log 及所在的页,首先会将 undo log 放入一个链表中,以供之后的 purge 操作。同时也会判断该 undo log 所在页是否可以给其他事务重用。

4.1 格式

4.1.1 insert undo log

insert 操作中产生的 undo log,因为是插入操作,只对事务本身可见,所以在提交后可直接删除不需要进行 purge 操作。

next:下一个 undo log 的位置,可以知道当前 undo log所占空间的字节数。

type_cmpl: undo log类型。

undo no:事务的 id

table_id: undo log 所对应的表。

主键的列和值:用来定位具体的记录。

4.1.2update undo log

deleteupdate 操作产生的 undo log。在事务提交时不能删除。

update vector表示 update 操作发生改变的列。

InnoDB 提供了:INNODB_TRX_ROLLBACK_SEGMENT 查询换回滚段信息, INNODB_TRX_UNDO 记录事务对应的 undo log(Mysql 5.6中找不到了...)。

5 group commit

redo log 事务提交的两阶段操作。

  1. 修改内存中事务对应的信息,并且将日志写入重做日志缓冲。
  2. 调用 fsync 将确保日志都从缓冲写入到磁盘中。

步骤2可以将多个事务的重做日志一次性 fsync 刷新到磁盘,减少磁盘的压力。在开启二进制日志后,为了保证存储引擎的事务和二进制日志的一致性,两者之间使用了两阶段事务:

  1. 当事务提交时,InnoDB 存储引擎进行 prepare 操作。

  2. MySQL 数据库写入二进制文件。

  3. InnoDB 存储引擎层将日志写入重做日志文件。

    a. 修改内存中事务对应的信息,并且将日志写入重做日志缓冲。

    b. 调用 fsync 将确保日志都从缓冲写入到磁盘中。

当步骤2操作完成后,事务就认为已经提交,即使步骤3执行时发生了宕机。步骤2、3都需要进行 fsync 操作确保数据的一致性。

5.1 Binary Log Group Commit

在 Mysql Server 层进行提交时,首先将事务放在一个队列中。

  1. Flush,将每个事务的二进制日志写入内存中。
  2. Sync,将内存中的二进制日志刷新到磁盘,若队列中有多个事务,可一次全部 fsync 到磁盘。
  3. Commit,按照顺序调用存储引擎事务的提交。

6 事务隔离级别

SQL标准定义了四个隔离级别

  1. READ UNCOMMITTED

  2. READ COMMITTED

  3. REPEATABLE READ

  4. SERIALIZABLE

InnoDB 默认的隔离级别是 REPEATABLE READ,同时使用了 Next Key 算法避免了幻读的产生,保证了事务的隔离性要求,达到了 SERIALIZABLE 隔离级别。

在 READ COMMITTED 隔离级别下,除了唯一性的约束检查需要 gap lock ,InnoDB 不会使用 gap lock 的锁。会有不可重复读和幻读的问题。

同时如果需要主从同步,且 bin log 使用了 STATEMENT 类型,会发生错误:

MASTER:

Session1Session2
BEGIN
DELETE FROM t WHERE a < 5;
BEGIN
INSERT INTO t VALUES (3);
COMMIT
COMMIT

在 Slave 上会发现 Session 中插入的数据也被删除了。在 MASTER 的执行顺序是先删后插,而 Statement 记录的却是先插后删逻辑上产生了不一致,如果 bin log 使用 Row 模式可以避免这个问题。

上次编辑于:
贡献者: blacklad