MySQL ACID与MVCC浅谈

前言

我们都知道在做事务的概念,就是一个完整的操作动作要么都执行,要么都不执行,这是一个不可分割的工作单位,ACID又是事务的四大特征。那么ACID具体是什么呢?

ACID介绍

原子性(atomicity)

一个事务必须被视为一个不可分割的最小工作单元,整个事务中即使包含几个步骤,但所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

只要新的事务T没有提交完成,那在不管在事务T发起前,还是操作过程等某一个时刻来获取数据库中的结果,结果都是一样的。

隔离性(isolation)

一个事务所做的修改在最终提交以前,其修改对其他事务是不可见的,这就是隔离性。

InnoDB支持的隔离级别有:

  • 读未提交(READ UNCOMMITTED)
  • 读提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 可串性化(SERIALIZABLE)

对于InnoDB默认的隔离级别是可重复读(REPEATABLE READ)。

持久性(durability)

一旦事务提交了,则其所做的修改就会永久保存到数据库中。即使此时系统崩溃,修改的数据也不会丢失。(当然持久化也分不同级别的)

隔离级别介绍

读未提交(READ UNCOMMITTED)

在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这称为脏读(Dirty Read)

时间 事务A 事务B
T1 开始事务 开始事务
T2 修改账号user1余额,将100改为200
T3 查询user1的余额,结果是200【脏读】
T4 操作出错,事务回滚
T5 从余额取出50,余额被更改为150(200-50)
T6 提交事务

备注:按照正确逻辑,此时账户余额应该是50。

这个级别会导致一些不太可靠的结果,从性能上来说,也不会比其他级别好太多。请谨慎使用此隔离级别,并注意结果可能不一致或无法重现,具体取决于其他事务同时执行的操作。通常,具有此隔离级别的事务仅执行查询,而不执行插入、更新或删除操作。

读提交(READ COMMITTED)

这个级别有时候也叫做不可重复读。只有事务提交了,才能被读取到,但是可能会存在两个阶段读取到的数值不一致的情况,就是在另外事务提交前读取的是一个数值,在事务提交之后读取到的又是另一个数值。这称为不可重复读(Nonrepeatable Read)

时间 事务A 事务B
T1 开始事务
T2 第一次查询,user1余额是100
T3 开始事务
T4 其他操作
T5 修改账号user1余额,将100改为200
T6 提交事务
T7 第二次查询,user1余额是200【不可重复读】
T8 继续其他操作

备注:按照正确逻辑,事务A前后两次读取到的数值应该一致。

在读提交隔离级别下,除了会出现「不可重复读」的情况,还会出现幻读(Phantom Read)

时间 事务A 事务B
T1 开始事务
T2 第一次查询,数据总量是100
T3 开始事务
T4 其他操作
T5 新插入100条数据
T6 提交事务
T7 第二次查询,数据总量为200条【幻读】
T8 继续其他操作

备注:按照正确逻辑,事务A前后两次读取到的数据总量应该一致。

可重复读(REPEATABLE READ)

InnoDB默认的隔离级别是可重复读。该级别的隔离,保证同一个事务中多次被读取同样数据结果是一致的。

可重复读解决了不可重复读 的问题。但是依旧不能解决幻读的情况。

InnoDB通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决幻读的问题。

不可重复读和幻读到底有什么区别呢?

  1. 不可重复读是读取了其他事务更改的数据,针对UPDATE操作

解决方式:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

  1. 幻读是读取了其他事务新增的数据,针对INSERT和DELETE操作

解决方式:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增/删除数据。

可串行化(SERIALIZABLE)

这是最高的隔离级别。通过强制事务每一个步串行执行,在读取的每一行数据上都加锁,所以可能导致大量的超时和抢锁问题。

实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用。

以上隔离级别从低到高,可以逐个解决脏读、不可重复读、幻读这几类问题。

隔离级别 脏读 不可重复读 幻读 加锁读
读未提交 Yes Yes Yes No
读提交 No Yes Yes No
可重复读 No No Yes No
可串行化 No No No Yes

MVCC介绍

概念

MVCC叫做多版本并发控制协议(Multiversion Concurrency Control),即多版本并发控制,它可以保存一行记录的多个历史版本(理解为快照),这些历史版本信息保存在 system tablespacesundo tablespaces 中,统一叫做 rollback segment。用这些信息来支持事物的回滚操作和一致性读(可重复读)。

MVCC 的读操作有两个概念:快照读当前读

  • 快照读:快照读的实现是基于MVCC,它读取的数据可能是历史数据。
  • 当前读:当前读即读取的是最新的数据,会对读取的记录进行加排它锁,保证读取时其它事务不能修改当前记录。

MVCC的核心概念主要是:

  • 每行数据会增加三个隐藏字段:DB_TRX_ID、DB_ROLL_PTR 和 DB_ROW_ID。
  • Undo logs
  • 视图(read view)

MVCC的设计就是为了实现读-写冲突不加锁,提高性能(这个读就是快照读,非当前读)。主要解决了不可重复读的问题,也解决了幻读的问题。

隐藏字段

InnoDB的MVCC是通过在每一行记录后面添加三个字段来实现:

  1. DB_TRX_ID。存储修改(插入、更新和删除)这行数据的最后一个事务的ID(6字节)。此外删除操作在内部视为更新操作,将该行中的特殊bit位标记为已删除。
  2. DB_ROLL_PTR。存储指向上一个版本数据在undo log 里的位置指针(7字节)。指向 rollback segment 中的一个回滚日志记录,如果一行被更新了,则回滚日志中记录了如何还原的信息。
  3. DB_ROW_ID。随着插入新行而单调增加的行ID(6字节)。当创建表没有合适的索引作为聚集索引而自动生成聚集索引时,会用该隐藏的行ID创建聚集索引。

所以当你插入一行记录,实际在InnoDB中应该是这样的:

id name age DB_TRX_ID DB_ROLL_PTR DB_ROW_ID
1 张三 15 5 344434 1

Undo logs

基本概念

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

存储方式

Undo log 存在于 undo log segment 中,undo log segment 存在于 rollback segment,rollback segment 存在于 system tablespace、undo tablespaces 和 temporary tablespaces。

innodb存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,InnoDB最多支持 128 个回滚段,其中 32 个分配给临时表空间。这留下了 96 个回滚段,可以分配给修改常规表中数据的事务。每个回滚段中有1024个undo log segment。

回收机制

Undo log 在 rollback segment 中,分为了插入日志和更新日志:

  1. 插入日志:只有在回滚时需要,事务提交后,该日志很快就被删除。
  2. 更新日志:用来进行一致性读,当没有事务需要时会被删除。

插入日志只要事务提交后,该日志就会很快删除。下面主要分析更新日志的内部机制。

当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。

但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。

通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)

  • delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
  • update分为两种情况:update的列是否是主键列。
    • 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
    • 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

⚠️注意:不能使用长事务,因为长事务会一直保留 undo log,这样日志文件会越来越大。

Read view

基本概念

视图(Read view)就是某个时间点的数据库快照,它的作用是定义事务执行期间”我能看见什么数据“。

在 MySQL 里,有两个视图的概念:

  • 一个是虚拟表,使用 create view 创建的。
  • 一个是 InnoDB 在实现 MVCC 时用到的一致性视图,用于支持 RC 和 RR 隔离级别的实现。

注意:视图的创建时间\

  1. 使用 BEGIN 或 START TRANSACTION 开启事务,然后会在第一个 SELECT 语句中进行创建。\
  2. 使用 START TRANSACTION WITH CONSISTENT SNAPSHOT 直接开启事物并创建快照。

重要名词概念:

  1. trx_id: 该行当前事务id。
  2. trx_ids: 当前系统活跃(未提交)事务版本号集合。
  3. min_trx_id: 创建当前read view 时系统正处于活跃的最小事务ID。
  4. max_trx_id: 创建当前read view 时系统应该分配的下一个事务ID。
  5. creator_trx_id: 创建当前read view的事务版本号。

判断逻辑

这样,对于当前事务的启动瞬间来说,一个数据版本的 DB_TRX_ID,有以下几种可能:

  1. 落在绿色区域。「事务ID < min_trx_id 」则显示。因为如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
  2. 落在红色区域。「事务ID > max_trx_id」则不显示。因为如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。
  3. 落在黄色区域。「min_trx_id < 事务ID < max_trx_id」则与trx_ids匹配。这时候会有三种情况:
    • 若事务ID不在集合中,表示这个版本是一件提交了的事务生成的,可见
    • 若事务ID在集合中,并且事物ID等于creator_trx_id,说明当前事务是自己生成的,所以可见
    • 若事务ID在集合中,但事物ID不等于creator_trx_id,说明当前事务不是自己生成的,并且该事务还没提交,所以不可见

Guess you like

Origin juejin.im/post/7035556545530167304