MySQL事务以及MVCC原理

Lou.Chen2022年11月3日
大约 13 分钟

数据库事务及四大特性

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:

  • **原子性(Atomicity)😗*事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
  • **一致性 (Consistency)😗*事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论事务执行成功否,参与转账的两个账号余额之和应该是不变的。
  • **隔离性(Isolation)😗*并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
  • **持久性(Durability)😗*事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。

原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。

  • 持久性是通过 redo log (重做日志)来保证的;
  • 原子性是通过 undo log(回滚日志) 来保证的;
  • 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  • 一致性则是通过持久性+原子性+隔离性来保证;

事务隔离级别操作

查看/修改MySQL的隔离级别

  • 查询全局事务隔离级别:SELECT @@global.tx_isolation
  • 查询当前会话事务隔离级别:
    • SELECT @@session.tx_isolation
    • SELECT @@tx_isolation
image-20221026142904941

可以发现「全局事务」和「当前会话事务」隔离级别都为**可重复读**

  • 修改MySQL隔离级别:SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]

关键字解释:

  • SESSION:当前会话
  • GLOBAL:全局会话
  • READ UNCOMMITTED:读未提交
  • READ COMMITTED:读已提交
  • REPEATABLE READ:可重复读
  • SERIALIZABLE:串行化

例如:设置当前会话事务的隔离级别为「读未提交」

SET SESSION TRANSACTION IOSLATION LEVEL READ UNCOMMITTED

事务开启提交回滚

查看/设置事务自动提交模式

查看事务自动提交模式:SHOW VARIABLES LIKE 'autocommit';

image-20221028172817265

autocommit 的值是 ON,表示系统开启自动提交模式

设置事务自动提交模式:SET autocommit = 0|1|ON|OFF;

  • 值为 0 和值为 OFF:关闭事务自动提交。如果关闭自动提交,用户将会一直处于某个事务中,只有提交或回滚后才会结束当前事务,重新开始一个新事务。
  • 值为 1 和值为 ON:开启事务自动提交。如果开启自动提交,则每执行一条 SQL 语句,事务都会提交一次。

开启/提交/回滚事务

  • 开启事务:

    • begin/start translation:执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机

    • start transaction with consistent snapshot:执行了 start transaction with consistent snapshot 命令,就会马上启动事务

  • 提交事务:commit

  • 回滚事务:rollback

使用 BEGIN 或 START TRANSACTION 开启一个事务之后,自动提交将保持禁用状态,直到使用 COMMIT 或 ROLLBACK 结束事务。之后,自动提交模式会恢复到之前的状态,即如果 BEGIN 前 autocommit = 1,则完成本次事务后 autocommit 还是 1。如果 BEGIN 前 autocommit = 0,则完成本次事务后 autocommit 还是 0。

事务并发产生的问题

脏读

如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。

例如:事务A事务读到事务B修改的数据,事务B还未提交到数据库,若后续事务B发生回滚,那么事务A读到此数据的事务后面操作此数据都是不正确的,这也就是脏数据

不可重复读

在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。

例如:事务A读到事务B提交之前的数据,然后事务B对该数据修改后提交事务,事务A再次读读取该数据后发现两次读取到的数据不一致。

幻读

在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就好像发生了幻觉一样,所以称为幻读。

例如:在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。

事务隔离级别分类

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
读已提交(read-committed)×
可重复读(repeatable-read)××
串行化(serializable)×××

读未提交(READ UNCOMMITTED)

在一个事务中可以读到另一个事务未提交的数据

产生脏读不可重复读幻读

读已提交(READ COMMITTED)

在一个事务中可以读到另一个事务中已经提交的数据

避免脏读

产生不可重复读幻读

事务T1不能够读取到T2未提交修改的数据。读已提交能够避免脏读。但是不能保证可重复读。

可重复读(REPEATABLE-READ)

可重复读为InnoDB默认的数据库隔离级别,在一个事务中不能够看到另一个事务未提交和已提交的事务,保证多次读取数据一致。

在事务开启的时候,对当前数据库拍一个快照,默认SELECT语句每次从快照中读取数据,从而保证每次读取到的数据一致,这种方式也叫快照读

避免脏读不可重复读

可能产生幻读,在某些情况下会产生幻读。

幻读出现的情况

可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读。例如以下两种情况:

情况一: 事务B执行后将B事务id存到该记录。接着事务A执行更新该条数据时将事务A的事务id存到该记录,所以事务A再次查询时能够看到数据

  • 事务A执行:select * from user where id=5; 结果为空
  • 事务B执行:insert into user values(5, '张三'); 然后提交事务
  • 事务A执行:update user set name='李四' where id=5
  • 事务A执行:select * from user where id=5 可以查询出id为5,name为李四的结果

情况二: 默认使用快照读数据后,再使用加X锁/S锁读方式变为当前读,即可以读到其他事务新增提交后的数据

  • 事务A进行普通SELECT查询,得到一个范围。
  • 事务B对该范围中增加一条数据并提交
  • 事务A使用SELECT加X锁/S锁的方式改为当前读,可以看到事务B新增的数据。
可重复读下解决幻读的方式

在可重复读的情况下事务开启时即手动对SELECT语句加X/S锁的方式,使快照读变为当前读,产生临键锁(记录锁+间隙锁)来阻塞其他事务对范围数据内的数据添加,从而避免幻读。

其他方式:将隔离级别改为SERIALIZABLE或者加表锁 (效率低,不推荐)

串行化(SERIALIZABLE)

会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

避免脏读不可重复读幻读

性能关系

image-20221028104615169

MVCC原理

https://mp.weixin.qq.com/s?__biz=MzI1NDY0MTkzNQ==&mid=2247495779&idx=1&sn=d2b8bbaac7bdaf035923a37bde27911c&chksm=e9c0a203deb72b155b7812f45bcf6df1bdb651d75a1b10bc5cadb1ee236872e1a22e2f769c31&scene=178&cur_album_id=1834236415635701764#rdopen in new window

https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247508002&idx=1&sn=9a59db214082ca8810dcdcb4af43c17c&chksm=f98de488cefa6d9ef82f7b17e5869cc1cb36a662d41d796ee85b84a9d5ae589bbd704b3a5b0d&scene=178&cur_album_id=1955634887135199237#rdopen in new window

undo log

undo log即回滚日志,当在事务中,每个INSERT操作时,就会生成一条相反的DELET语句的undo log日志。每个DELETE操作时,也会生成一条修改为原始值的UPDATE语句的undo log日志。

undo lgo日志包含数据id,事务id等信息,方便事务回滚的时候找到该条日志后执行相应的回滚语句,从而实现事务回滚。

行格式

行格式就是 InnoDB 在保存每一行的数据的时候,究竟是以什么样的格式来保存这行数据的。

一般一行数据会有以下隐藏的数据列:

image-20221103134747664

上图中的列 1、列 2、列 3 一直到列 N,就是我们数据库中表的列,保存着正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这里要重点关注的 DB_ROW_IDDB_TRX_IDDB_ROLL_PTR 三列:

  • DB_ROW_ID:该列占用 6 个字节,是一个行 ID,用来唯一标识一行数据。如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引。
  • DB_TRX_ID:该列占用 6 个字节,是一个事务 ID。在 InnoDB 存储引擎中,当要开启一个事务的时候,会向 InnoDB 的事务系统申请一个事务 id,这个事务 id 是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务 id 记录在当前行中。
  • DB_ROLL_PTR:该列占用 7 个字节,是一个回滚指针,这个回滚指针指向一条 undo log 日志的地址,通过这个 undo log 日志可以让这条记录恢复到前一个版本。

MVCC基本概念

MVCC(Multi-Version Concurrency Control):多版本并发控制

MVCC实现读已提交可重复读来尽量避免出现的并发问题,它们是通过 Read View(快照读) 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把

Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。

  • 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,也就不能保证可重复读和幻读
  • 「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View,所以能保证可重复读,但是不能避免一些情况的幻读

MVCC 的核心思路就是保存数据行的历史版本通过对数据行的多个版本进行管理来实现数据库的并发控制

image-20221103135524073

Read View工作原理

当执行INSERT/UPDATE/DELETE操作数据的时候,会将本事务id保存在数据行的DB_TRX_ID字段中,也就是哪个数据行被哪个事务修改都会有记录

并且执行INSERT/UPDATE/DELETE操作数据的时候都会产生相应的undo log回滚日志,并将回滚日志地址存储在数据行的DB_ROLL_PTR的字段中,且每次修改都会执行上一个记录的版本,所以每次执行undo log就会将数据恢复到上一个版本。

Read View结构:

image-20221104140839141
  • creator_tex_id:当开启事务的时候,会向InnoDB事务系统中申请一个事务id,并且事务id是一个严格按照事务申请的先后顺序递增的一个数字

  • m_ids:在事务开启的时候系统会创建一个事务数组数组中包含所有活跃的事务id,也就是开启了事务但是还没提交的事务。

  • min_trx_id:活跃事务列表中最小的事务id

  • max_trx_id:这个并不是活跃事务中的最大值,而是创建 Read View 时,目前最大事务id+1,即下一个将分配的事务id

    从申请到 trx_id 到创建数组之间也是需要时间的,这期间可能有其他会话也申请到了 trx_id。

每次开启Read View后,查询数据行时,会一直会沿着当前最新数据行的事务idundo log地址向上查找,直到查询出满足的版本链数据或者返回为空为止。

行数据事务id在Read View中出现的情况而造成的数据可见性问题如下:

image-20221104142058311
  • 如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见

  • 如果这个值小于活跃事务中的最小id,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行是可见的

    行数据事务id:5 当前事务:6 活动事务:[6,7] 当前事务开启时,id为5的事务已提交,所以可见。

  • 如果这个值大于等于事务最大id,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时该行数据储存的就是修改改行的事务id,所以当前事务对此行数据不可见

    行数据事务id:8 当前事务:6 活动事务:[6,7] 开启当前事务后,发现后面开启的事务id为8的事务已经对改数据进行修改了,所以不可见

  • 如果这个值的大于活跃事务最小id并且小于最大事务id

    • 该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。

      行数据事务id:5 当前事务:6 活动事务:[4,6] 开启当前事务后,其他事务对其修改的已提交,可见

    • 该值在数组中,说明这是一个未提交的事务修改的数据,不可见

      行数据事务id:5 当前事务:6 活动事务:[4,5,6] 开启当前事务后,其他事务对其修改的未提交,不可见

读已提交和可重复读区别
  • 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
  • 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。