Skip to main content

事务原理

David LiuAbout 9 min

事务原理

事务性质

截屏2023-02-24 23.15.51

redo log

保证:持久性:事务一旦提交或回滚,他对数据库中数据的改变就是永久的。

物理日志:存操作的指令

该日志文件由两部分组成:

  • 重做日志缓冲(redo log buffer)
  • 重做日志文件(redo log file)

前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

WAL,先写日志,再往磁盘写,这样可以先记录下来

保证刷新脏页发生错误时,可以恢复数据

既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

  1. 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是追加操作,属于顺序 IO。
  2. 刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小修改都要整页写入;而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少。

undo log

保证:原子性:事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

作用:

  • 提供回滚
  • MVCC

逻辑日志:存操作的指令的反向操作(操作以前会把历史版本记入)

  • Undo log 销毁:undo log 在事务执行时产生,事务提交时,并不会立即别除 undo log,因为这些日志可能还用于 MVCC。
  • Undo log 存储:undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含 1024 个 undo log segment。

MVCC

保证:隔离性:保证事务在不受外部并发操作影响的独立环境下运行。

实现:隔离级别

概念

MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为 MySQL 实现。

MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于:

  • 数据库聚簇索引记录中的三个隐式字段
  • undo log 日志
  • read view

当前读/锁定读

特点:读取的记录是最新版本,加锁防止别人修改。(可以完全解决插入或删除导致的幻读)

  • select xxx lock in share mode
  • select ... for update
  • insertupdatedelete 操作

快照读/一致性非锁定读

简单的 select (不加锁) 就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

  • Read Committed: 每次 select,都生成一个快照读。
  • Repeatable Read: 开启事务后第一个 select 语句才是快照读的地方。
  • Serializable: 快照读会退化为当前读。

在一致性非锁定读下,即使读取的记录已被其它事务加上 X 锁,这时记录也是可以被读取的,即读取的快照数据。

上面说了,在 Repeatable ReadMVCC 防止了部分幻读,这边的 “部分” 是指在 一致性非锁定读 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是如果是 当前读 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, InnoDB 在实现Repeatable Read 时,如果执行的是当前读,则会对读取的记录使用 Next-key Lock ,来防止其它事务在间隙间插入数据

隐藏字段

  • DB_TRX_ID(6字节):表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
  • DB_ROLL_PTR(7字节): 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
  • DB_ROW_ID(6字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

ibd2sdi xxx.ibd可以查看这张表的定义格式,可以看到所有的字段包括隐藏字段

undo log

undo log 中是一个版本链(链表)

不同事务或相同事务对同一条记录进行修改,会导致该记录的 undolog 生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

作用

undo log 主要有两个作用:

  • 当事务回滚时用于将数据恢复到修改前的样子
  • 另一个作用是 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读(快照读)

分类

undo log 分为两种: insert undo logupdate undo log

  1. insert undo log :指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作
  2. update undo logupdatedelete 操作中产生的 undo log。该 undo log可能需要提供 MVCC 机制(快照读),因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除

Read View

读视图,是快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活动的事务(未提交的)id。确定当前快照读应该读哪个版本。 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”。

核心字段

ReadView 中包含了四个核心字段:

字段含义
m_ids当前活跃的事务 ID 集合,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
min_trx_id最小活跃事务 ID,即m_ids 的最小值
max_trx_id预分配事务 ID,当前最大事务 ID+1(因为事务 ID 是自增的)
creator_trx_id该 ReadView 创建者的事务 ID
m_low_limit_no事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge

截屏2023-02-25 13.01.59

数据可见性算法

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 值在 Read View 的min_trx_idmax_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 如果记录的 trx_id m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
    • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见

遇到不可见的记录就会利用 DB_ROLL_PTR 沿着undo log版本链往下找,直到找到满足的快照版本或返回空。

生成时机

不同的隔离级别,生成 Readview 的时机不同:

  • READ COMMITTED:会在事务中每一次执行快照读时生成 Read View。
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成 Read View,后续复用该 Read View,(小林中说是事务启动的时候生成)

未解决的幻读情况

锁定读下完全解决了因插入和删除导致的幻读,但是非锁定读的情况下没有完全解决。

情况一:快照读

img

情况二:当前读

  • T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
  • T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
  • T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

一致性保证

一致性是事务追求的最终目标,实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证.
  • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
  • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致.

CAP 和 ACID 中的一致性

  • CAP 定理中的数据一致性,其实是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值
  • ACID 中的一致性是指数据库的规则,如果 schema 中规定了一个值必须是唯一的,那么一致的系统必须确保在所有的操作中,该值都是唯一的。

由此来看 CAP 和 ACID 对于一致性的定义有着根本性的区别。

总结事务的 ACID 四大基本特性是保证数据库能够运行的基石,但是完全保证数据库的 ACID,尤其是隔离性会对性能有比较大影响,在实际的使用中我们也会根据业务的需求对隔离性进行调整,除了隔离性,数据库的原子性和持久性相信都是比较好理解的特性,前者保证数据库的事务要么全部执行、要么全部不执行,后者保证了对数据库的写入都是持久存储的、非易失的,而一致性不仅是数据库对本身数据的完整性的要求,同时也对开发者提出了要求-写出逻辑正确并且合理的事务。最后,也是最重要的,当别人在讲一致性的时候,一定要搞清楚他的上下文。