本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

四种:事务隔离级别

  • 读未提交(READ UNCOMMITTED):一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(READ COMMITTED):一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(REPEATABLE READ):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化(SERIALIZABLE):所有select语句都会被隐式加共享锁:select … in share mode,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于SELECT语句,InnoDB不会主动加任何锁;串行化隔离级别下,会自动给select也加锁(共享锁,也包括间隙锁)
串行化隔离级别下,就不会出现脏读,不可重复读,幻读等问题。因为通过加锁,强制事务排序等待了了。但是效率很低。

隔离级别解决了哪些问题?

隔离级别针对快照读的情况也就是不加锁的读,
update修改记录或者给select加锁都属于当前读,会强制事务排序,所以不会出现下面的情况。

  • 脏读(dirty read):如一个事务读到了另一个未提交事务修改过的数据。

  • 不可重复读(non-repeatable read):如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。

  • 幻读(phantom read):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来或者修改掉。

如何设置事务的隔离级别?

我们可以通过下边的语句修改事务的隔离级别:

  1. SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;//等级就是上面的几种

事务怎么启动的?

启动事务一般就是:

  1. 显式启动事务语句, begin 或 start transaction,配套的提交语句是 commit,回滚语句是 rollback。
  2. 自动启动事务:set autocommit=0,这个命令会将这个线程的自动提交关掉,意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

视图

首先得说一下,我后面所有的知识都是基于InnoDB的,因为 MyISAM 不支持事务。

视图,这是事务隔离实现的根本,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

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

  • 一个是 view,它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
  • 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

可重复读隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。

在正式介绍之前,我还需要介绍一下版本链

版本连

可重复读隔离级别下,事务在启动的时候就 “拍了个快照”,注意,这个快照是基于整库的。

你肯定会说,这怎么可能?如果一个库有 100G,那么我启动一个事务,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊?

实际上,数据库并不需要拷贝出这 100G 的数据

那快照怎么实现的?

InnoDB 里面每个事务有一个唯一的事务 ID,叫作transaction id,它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

trx_id

每行数据也都是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的trx_id 隐藏列。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的trx_id

roll_pointer

还有另外一个隐藏列roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo回滚日志中,然后这个隐藏列就相当于一个指针,可以通过它来在undo回滚日志中找到该记录修改前的信息

两者都在 InnoDB 的聚簇索引中,大概就长这样:

MVCC和事务隔离级别的关系 - 图1

undo log的回滚机制也是依靠这个版本链,每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个roll_pointer属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:

MVCC和事务隔离级别的关系 - 图2

InnoDB 引擎就是利用每行数据有多个版本的特性,实现了秒级创建 “快照”,并不需要花费大量的是时间。

事务隔离级别和 MVCC 的关系

读提交隔离级别

读提交隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的,在这个隔离级别下,当前事务中,每次查询时都会生成一个独立的 ReadView。所以读提交隔离级别下,一个事务中多次查询,会出现不一样的结果。

MVCC和事务隔离级别的关系 - 图3

可重复读 隔离级别

可重复读隔离级别,在第一次读取数据时生成一个 ReadView,对于使用REPEATABLE READ隔离级别的事务来说,只会在当前事务中第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了,本次事务中,都是用的这个版本的数据;所以一个事务的查询结果每次都是一样的。

MVCC和事务隔离级别的关系 - 图4

和mvcc无关的隔离级别

读未提交

读未提交隔离级别下直接返回记录的最新值,没有视图概念,脏读,幻读,不可重复读都有可能发生。

串行化

串行化隔离级别下直接用加锁的方式来避免并行访问。使事务串行化访问。

选择哪种隔离级别

读未提交是没有加任何锁的,所以对于它来说也就是没有隔离的效果,所以它的性能也是最好的。
串行化给所有的select都默认加锁了,性能最差。
读提交和可重复读,这两个隔离级别下,使用了mvcc多版本并发控制。读数据的时候需要将当前版本的数据写入到undo回滚日志中;需要增加两个隐藏的列。一个是事务版本号,一个指向undo回滚日志中,

我工作以来,我所有接触的公司的数据库隔离级别默认都是读已提交,不过我们会在一些场景开启可重复读,序列化很少见。

可重复读我们之前都是在跟订单金额相关的场景去开启的,还有很多数据修改过程也会用可重复度,因为很多值是需要查询出来,依据那个值做别的操作的,如果多次查询的结果值不一样,那后者也会受到影响。

序列化被称为数据库隔离级别的黄金标准,它是绝大多数商业数据库系统中提供的最高隔离级别,金融的场景居多,性能也是最差的,但是银行取钱你会在乎那几秒么?
有没有发现银行的 ATM 响应速度特别慢,他们的场景都是很严密的,各种事务,锁,都是结合的,就是为了保证结果的准确性。

大家可以用这个命令去看看自己公司或者自己现在使用的数据库的隔离级别:

show variables

MVCC和事务隔离级别的关系 - 图5

资料参考:《MySQL 是怎样运行的:从根儿上理解 MySQL》、《高性能 MySQL》、《 MySQL 实战 45 讲》

mvcc总结

MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用读已提交(READ COMMITTD)、可重复读(REPEATABLE READ)这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读 - 写、写 - 读操作并发执行,从而提升系统性能。

这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,数据的可重复读其实就是 ReadView 的重复使用。而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView

在rr隔离级别下,通过mvcc可以避免快照读的幻读,

可重复读隔离级别下的这种mvcc的一致性视图,解决了不加锁的select(快照读)的可重复读问题。但可能出现幻读问题,因为select的时候没有加锁,其它事务就有可能修改select的结果,导致select后续的操作幻读了。这个时候可以通过给select加锁的方式强制事务排序(加锁之后会存在记录锁和间隙锁,数据间隙部分也会锁住),避免出现幻读。
幻读场景:
如一个事务中通过唯一索引如手机号,进行普通select查询,查询到没有这条记录,开始新增记录
另一个事务中进行insert操作(当前读)和事务1想要插入的记一样录,且提交了事务。
此时事务一执行insert操作,就会报错,因为记录存在。此时对于事务1来说,就发生了幻读的情况。
所以可以对事务一的select进行加共享锁或者排它锁,唯一索引等值查询(记录不存在)有间隙索,此时事务2的写操作就会阻塞,保证事务1不会发生幻读
MySQL是如何解决幻读