MySQL之ACID实现原理

MySQL之ACID实现原理

1. MySQL事务

事务是一组操作的集合,它是一个不可分割的最小的工作单位,事务会把所有的操作作为一个整体一起向系统提交或者撤销操作请求,即这些操作要么同时成功,要么同时失败。

MySQL中关于事务操作的命令如下:

select @@autocommit; # 查看事务的提交方式
set @@autocommit; # 设置事务的提交方式
commit; # 提交事务
rollback; # 回滚事务
start transaction; # 开启事务
begin; # 开启事务

2. 事务四大特性

事务的四大特性其实就是我们常说的ACID,对于它们的解释如下:

(1)原子性(atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。事务是操作的最小单位,不可再分。

(2)一致性(consistency):数据库总是从一个一致性转换到另一个一致性。即事务发生前和事务发生后,数据的完整性必须保持一致。

(3)隔离性(isolation):多个事务之间的数据是相互隔离的,当并发访问数据库时,一个正在执行的事务的修改对其他事务时不可见的。

(4)持久性(durability):一个事务一旦被提交或者回滚,它对数据库中的数据的改变是永久性的。即使出现错误,事务也不允许撤销,只能通过“补偿性事务”。

3. 如何实现ACID

原子性:通过undo log来保证的。

持久性:通过redo log来保证的。

隔离性:通过MVCC或者锁机制来保证的。

一致性:通过持久性 + 原子性 + 隔离性来保证的。

3.1 原子性

事务的原子性是通过undo log来实现,undo log回滚日志,用于记录数据被修改前的信息。(保证事务的原子性)

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条数据时,它会记录一条对应相反的update记录。当rollback执行时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

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

Undo log的作用有两个:

  • 保证数据的原子性,记录事务发生之前的版本,用于回滚。
  • 通过MVCC+undo log版本链实现InnoDB事务中可重复读和读已提交隔离级别。

3.2 持久性

事务的持久性是通过redo log实现的redo log是重做日志,记录的是事务提交时数据页的物理修改。(解决事务的持久性)

该日志由两部分组成:重做日志缓冲(redo logo buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有的修改信息全部存到该日志文件中,用于在刷新脏页到磁盘中,发生错误时,进行数据恢复使用。

image-20240325160219917

为什么每次提交要把redo log直接刷新到磁盘中?

如果每次提交直接把Buffer Pool中的数据刷新到磁盘文件中,会存在严重的性能问题。因为通常在事务中,会进行多条操作,这些操作都是随机去操作数据页的,这时候就会涉及到大量的随机磁盘IO。

如果是在事务提交的时候,不把脏页数据直接刷新到磁盘IO,而是先把redo log日志文件异步刷新到磁盘中,由于日志文件都是以追加的方式写入磁盘的,那么这时候就是顺序磁盘IO,性能是比随机IO高的,这也被称为WAL(Write-Ahead Logging)。

3.3 隔离性

事务的隔离性通过MVCC+锁来实现的。

(1)隔离级别

事务的隔离级别有四种:

  • **读未提交(read uncommitted)**:指一个事务还没提交时,它做的变更就能被其他事务看到。读未提交的数据,称之为脏读(Dirty Read)。
  • **读已提交(read committed)**:指一个事务提交之后,它做的变更才能被其他事务看到。这种隔离级别支持不可重复读(Nonrepeatable Read),因为同一事务在不同时期读取到的数据可能是不一样的。
  • 可重复读(repeatable read):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别。但是会产生幻读的问题,当前事务读取某一范围的数据时,另一个事务又在该范围内插入了新行,用户再次读取时,会出现新的“幻影”。
  • **可串行化(serializable )**:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

查看隔离级别

select @@transaction_isolation;

设置隔离级别

set session transaction isolation level read uncommitted;

(2)并发事务问题

脏读:脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。

不可重复读:对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新操作。

幻读:幻读是针对数据插入操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,让用户感觉感觉出现了幻觉,这就叫幻读。

可重复读:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。

不可重复读与幻读的区别

不可重复度侧重于读取到其他事务修改的数据,描述的通常是数据更新操作下的问题;

幻读侧重于读取到其他事务新增或者删除的数据,描述的通常是数据插入操作下的问题。

(3)锁机制

MySQL锁机制的基本工作原理就是:事务在修改数据库之前,需要先获得相应的锁,获得锁的事务才可以修改数据;在该事务操作期间,这部分的数据是锁定,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

  • 排它锁解决脏读:在RC隔离级别下,事务A只有在对数据修改时才加排它锁,但直到事务 commit 时才释放锁。因此,同时进行的事务B希望读取同一行数据时,会被事务A的排它锁堵塞,所以解决了脏读的问题。
  • 共享锁解决不可重复读:在RR隔离级别下,除了执行读已提交的排它锁方式,还会在读取一行数据时,为这行数据添加共享锁直至事务 commit。例如,事务A读取ID=1这一行数据,然后为ID=1添加共享锁,事务B同时希望update ID=1,此时获取写锁失败,因此在事务A执行完之前,没有其他任何事务可以对ID=1这一行做修改,因此解决了重复读的问题。
  • 临建锁解决幻读:默认情况下,InnoDB在RR事务隔离级别运行,InnoDB使用next-key锁进行搜索和索引扫描,以防止幻读。

(4)MVCC

全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段undo log日志readView

学习MVCC之前我们先来了解一下两个基本的概念。

当前读

读取的时记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

对于我们日常的操作,如

  • select... lock in share mode(共享锁)
  • select ... for update
  • updateinsertdelete(排他锁)

快照读

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

  • Read Commited 读已提交:每次select,都生成一个快照读。
  • Repeatable Read 可重复读:开启事务后的第一个select语句才是快照读的地方。
  • Serializable 可串行化 :快照读会退化为当前读。

三个隐式字段

  • DB_TRX_ID:最近修改事务ID,记录插入这条记录或者最后一次修改该记录的事务ID。
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配置undo log,指向上一个版本。
  • DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐式字段。
// 查看数据库文件,在表结构字段中就可以看到隐式字段的相关信息
ibd2sdi xx.ibd

undo log版本链

undo log就是回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

  • 在insert的时候,产生的undo log日志只在回滚的时候需要,在事务提交后,可被立即删除。
  • 而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

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

image-20240325162017164

ReadView

ReadView(读视图)是快照读SQL执行时MVCC提取的数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

ReadView中包含四个核心字段:

  • m_ids:当前活跃的事务ID集合。
  • min_trx_id:最小活跃事务ID。
  • max_trx_id:预分配事务ID,当前最大事务ID + 1,因为事务ID是自增的。
  • creator_trx_id:ReadView创建者的事务ID。

版本链访问规则

我们假设当前事务ID为trx_id,则

trx_id == creator_trx_id,可以访问该版本,因为数据操作是当前事务完成的。

trx_id < min_trx_id,可以访问该版本,因为min_trx_id事务已经在当前事务之前提交了,数据修改已经提交了。

min_trx_id <= trx_id <= max_trx_id, 如果当前事务trx_id不在m_ids中是可以访问该版本的,因为数据修改已经提交了。

读视图的生成规则

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

  • Read Committed:在事务中每执行一次快照读时生成ReadView。
  • Repertable Read:仅在事务中第一次执行快照读时生成ReadView,后续复读该ReadView。

3.4 一致性

事务的一致性是通过 原子性+持久性+隔离性 来实现的,也就是说,一致性才是最终事务的目的。

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的,主要基于undo log实现。
  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失,主要基于redo log实现。
  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)。

image-20240325160508794