MySQL—事务和MVCC

0. 简介

并不是所有MySQL的引擎都支持事务,现阶段只有InnoDB和NDB支持事务。

事务必须满足以下四点:

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

接下来,我们就重点介绍事务隔离性的保障机制之一------MVCC(多版本并发控制)。

1. 事务的隔离级别

1.1 事务并发时遇到的问题

可参考:大白话讲解脏写、脏读、不可重复读和幻读,事务并发执行时,可能会遇到一些问题,按照严重性从高到低排序:

  • 脏写(Dirty Write):一个事务修改了另一个没有提交的事务的值;
  • 脏读(Dirty Read):一个事务读到了另一个未提交的事务的值;
  • 不可重复读(Non-Repeatable Read):一个事务内多次读取同一个数据,出现前后两次数据不一致的情况;
  • 幻读(Phantom):在一个事务内多次查询到符合条件的记录数量不一致。

1.2 SQL定义的4种隔离级别

SQL标准中,定义了4种隔离级别,隔离级别越低,越严重的问题就可能发生,隔离级别从低到高如下所示:

隔离级别 脏写 脏读 不可重复读 幻读
READ UNCOMMITTED(未提交读) No 可能 可能 可能
READ COMMITTED(已提交读) No No 可能 可能
REPEATABLE READ(可重复读) No No No 可能
SERIALIZABLE(串行化) No No No No

从上表可以看出:

  • 所有的级别都不能忍受脏写;
  • 串行化安全级别最高,所有的问题都不会出现,但是串行化的效率太低,拉低了系统性能。

1.3 MySQL中支持的四种隔离级别

MySQL虽然支持以上的四种隔离级别,但是和SQL标准中的区别在于:MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象:

  • 针对快照读(普通select语句),通过MVCC方式解决幻读;
  • 针对当前读 (select ... for update 等语句),通过加next-key lock解决幻读。

MySQL中的默认隔离级别是REPEATABLE-READ:

sql 复制代码
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec)

2. 多版本并发控制 MVCC

MVCC(Multi-Version Concurrency Control),即「多版本并发控制」,指的是在使用 READ COMMITTD 和 REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时,访问记录的版本链的过程。

通过 MVCC,可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。

2.1 版本链

对于InnoDB存储引擎来说,其记录中都含有两个必须得隐藏列:trx_id和roll_pointer:

  • trx_id:每次一个事务对某条记录进行改动时,都会把该事物的id赋值给trx_id列;
  • roll_pointer:每次对某条记录进行改动时,都会把旧版本的记录写入到undo日志中,然后roll_pointer列就相当于一个指针,指向undo页中原来的记录,如果有多次修改,那么就会形成一个版本链。

2.2 ReadView

对于READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到的已经提交了的事务修改多的记录,也就是说,假如另一个事务修改了记录,但是尚未提交,那么是不能读取到其版本的。核心问题就是:需要判断一下版本链中哪个版本是当前事务可见的。

为此,引入了一个ReadView的概念,其组成如上图所示:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是 m_ids中的最小值。

  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值,注意这就不是m_ids中的最大id了。

  • creator_trx_id:指的是创建该 Read View 的事务的事务id。

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

  • 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前 已经提交的事务生成的,所以该版本的记录对当前事务可见。

  • 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后 才启动的事务生成的,所以该版本的记录对当前事务不可见。

  • 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:

    • 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
    • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

当判定该版本记录不可见时,会沿着版本链一直读下去,直到遇到可见的版本,或者没有记录。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

在MySQL默认的隔离级别REPEATABLE READ下,在事务启动后第一次读取数据时 生成一个ReadView,这就保证在此级别下,整个事务期间读到的记录都是第一次读之前的记录,保证不会存在不可重复读问题。

在隔离级别READ COMMITTD下,每次读数据之前,都会生成一次ReadView,这就导致其并不能避免不可重复读问题,但是可以避免脏读。

2.3 如何尽量避免幻读 / 是否完全避免幻读了?

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:

  • 针对快照读 (普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读 (select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。

举两个例子:

  • 对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。

  • 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

所以说,MySQL的REPEATABLE READ隔离级别只是尽最大可能地避免了幻读,但是没有完全避免!

3. 小结

其实本质上,MVCC的版本链控制,是一个乐观锁设计,保证数据库的高效性。

4. 参考文档

MySQL事务和MVCC

小林coding-事务隔离级别是怎么实现的?

《MySQL是怎样运行的》从根儿上理解 MySQL

相关推荐
骑着蜗牛撵大象32722 分钟前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 里的建表与增删改查手法
数据库·sql·mysql·编辑器·crud·执行计划
CaptainT42 分钟前
WHERE 到底筛掉了哪些行
sql·mysql
CaptainT1 小时前
先把例子跑起来:MySQL 环境与本文的示例数据
数据库·后端·mysql
夜雪一千1 小时前
MySQL中查询和操作JSON类型的数据
数据库·mysql·json
for_ever_love__2 小时前
MySQL 8.0 表设计规范:从业务建模、约束到索引评审的完整方法
数据库·mysql·表设计·建表规范
for_ever_love__12 小时前
MySQL ORDER BY 排序优化:从 Using filesort 到索引有序扫描(含双路 / 单路排序原理)
java·mysql·优化·存储·by·order·filesort
Sayai15 小时前
DataX 离线数据同步实战:Elasticsearch 数据批量导出 MySQL(作业配置、并发调优与定时同步平台化设计)
大数据·mysql·elasticsearch·datax·数据同步
颜酱16 小时前
一栋商业大厦,讲清 InnoDB 的表空间、段、区和页
后端·mysql
PC2005-cloud18 小时前
Mysql学习笔记:InnoDB Cluster 集群从零搭建实战,基于 Docker 实现高可用读写分离架构
笔记·学习·mysql
这个DBA有点耶18 小时前
分布式事务深度:TCC空回滚、悬挂与Saga补偿风暴的根因与解法
数据库·mysql·代码规范