---
# System prepended metadata

title: MySQL 事务的隔离级别
tags: [MySql]

---

# MySQL 事务的隔离级别
https://developer.aliyun.com/article/743691

https://www.bilibili.com/video/BV1Vt4117783?p=7
## MySQL 中执行事务

事务的执行过程如下，以 begin 或者 start transaction 开始，然后执行一系列操作，最后要执行 commit 操作，事务才算结束。当然，如果进行回滚操作(rollback)，事务也会结束。

![](https://i.imgur.com/mXGO3pc.png)

需要注意的是，begin 命令并不代表事务的开始，事务开始于 begin 命令之后的第一条语句执行的时候。例如下面示例中，select * from xxx 才是事务的开始，

```
begin;
select * from xxx; 
commit; -- 或者 rollback;
```
另外，通过以下语句可以查询当前有多少事务正在运行。

`select * from information_schema.innodb_trx;`


開始可以用 set autoCommit 代表每句都不自動提交 要commit

或用 set transaction 最通用的用法

事務是當前用戶用insert 之類的 別人看不到
要commit才看得到

所以跟索要一起
這樣當你做事務 rollback才知道回去哪裡


## 介紹
本文所说的 MySQL 事务都是指在 InnoDB 引擎下，MyISAM 引擎是不支持事务的。

数据库事务指的是一组数据操作，事务内的操作要么就是全部成功，要么就是全部失败，什么都不做，其实不是没做，是可能做了一部分但是只要有一步失败，就要回滚所有操作，有点一不做二不休的意思。

假设一个网购付款的操作，用户付款后要涉及到订单状态更新、扣库存以及其他一系列动作，这就是一个事务，如果一切正常那就相安无事，一旦中间有某个环节异常，那整个事务就要回滚，总不能更新了订单状态但是不扣库存吧，这问题就大了。

事务具有原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）、持久性（Durability）四个特性，简称 ACID，缺一不可。今天要说的就是隔离性。


## 概念说明
以下几个概念是事务隔离级别要实际解决的问题，所以需要搞清楚都是什么意思。

### 脏读（Dirty Read）
![](https://i.imgur.com/TJ5hzLC.png)

会话B开启一个事务，把id=1的name为武汉市修改成温州市，此时另外一个会话A也开启一个事务，读取id=1的name，此时的查询结果为温州市，会话B的事务最后回滚了刚才修改的记录，这样会话A读到的数据是不存在的，这个现象就是脏读。（脏读只在读未提交隔离级别才会出现）


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

### 不可重复读（Non-Repeatable Read） （前后多次读取，数据内容不一致）
一个事务只能读到另一个已经提交的事务修改过的数据，并且其他事务每对该数据进行一次修改并提交后，该事务都能查询得到最新值。（不可重复读在读未提交和读已提交隔离级别都可能会出现）

![](https://i.imgur.com/oFIjNJg.png)

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

### 可重复读

可重复读指的是在一个事务内，最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新（UPDATE）操作。

### 幻读（Phantom）
一个事务先根据某些条件查询出一些记录，之后另一个事务又向表中插入了符合这些条件的记录，原先的事务再次按照该条件查询时，能把另一个事务插入的记录也读出来。（幻读在读未提交、读已提交、可重复读隔离级别都可能会出现）

![](https://i.imgur.com/iE63cAH.png)
会话A开启一个事务，查询id>0的记录，此时会查到name=武汉市的记录。接着会话B插入一条name=温州市的数据（隐式事务，因为此时的autocommit为1，每条SQL语句执行完自动提交），这时会话A的事务再以刚才的查询条件（id>0）再一次查询，此时会出现两条记录（name为武汉市和温州市的记录），这种现象就是幻读。

## 事务的隔离级别

MySQL的事务隔离级别一共有四个，分别是读未提交、读已提交、可重复读以及可串行化。

MySQL的隔离级别的作用就是让事务之间互相隔离，互不影响，这样可以保证事务的一致性。

隔离级别比较：可串行化>可重复读>读已提交>读未提交

隔离级别对性能的影响比较：可串行化>可重复读>读已提交>读未提交

由此看出，隔离级别越高，所需要消耗的MySQL性能越大（如事务并发严重性），为了平衡二者，一般建议设置的隔离级别为可重复读，MySQL默认的隔离级别也是可重复读。

## 读未提交（READ UNCOMMITTED）
![](https://i.imgur.com/bUTuT3o.png)
在读未提交隔离级别下，事务A可以读取到事务B修改过但未提交的数据。

可能发生脏读、不可重复读和幻读问题，一般很少使用此隔离级别

## 读已提交（READ COMMITTED）

MySQL 事务隔离其实是依靠锁来实现的，加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的，所以它的性能是最好的，没有加锁、解锁带来的性能开销。但有利就有弊，这基本上就相当于裸奔啊，所以它连脏读的问题都没办法解决。

任何事务对数据的修改都会第一时间暴露给其他事务，即使事务还没有提交。

![](https://i.imgur.com/v42aoh3.png)
在读已提交隔离级别下，事务B只能在事务A修改过并且已提交后才能读取到事务B修改的数据。

读已提交隔离级别解决了脏读的问题，但可能发生不可重复读和幻读问题，一般很少使用此隔离级别。


在RC级别中，数据的读取都是不加锁的，但是数据的写入、修改和删除是需要加锁的。效果如下

![](https://i.imgur.com/kU2tauw.png)
## 可重复读（REPEATABLE READ）
MySQL的InnoDB默认是使用的RR级别
![](https://i.imgur.com/YMFw4J7.png)
在可重复读隔离级别下，事务B只能在事务A修改过数据并提交后，自己也提交事务后，才能读取到事务B修改的数据。

可重复读隔离级别解决了脏读和不可重复读的问题，但可能发生幻读问题。

提问：为什么上了写锁（写操作），别的事务还可以读操作？

因为InnoDB有MVCC机制（多版本并发控制），可以使用快照读，而不会被阻塞。

## 可串行化（SERIALIZABLE）
![](https://i.imgur.com/HIkXTpF.png)
![](https://i.imgur.com/x29X1ga.png)
![](https://i.imgur.com/9loePVq.png)
![](https://i.imgur.com/m9PWjsX.png)
各种问题（脏读、不可重复读、幻读）都不会发生，通过加锁实现（读锁和写锁）
![](https://i.imgur.com/mcxQmyf.png)
![](https://i.imgur.com/MeNVAPm.png)


串行化是4种事务隔离级别中隔离效果最好的，解决了脏读、可重复读、幻读的问题，但是效果最差，它将事务的执行变为顺序执行，与其他三个隔离级别相比，它就相当于单线程，后一个事务的执行必须等待前一个事务结束。



## 快照
提交和可重复读的时候都提到了一个词

学名叫做一致性视图，这也是可重复读和不可重复读的关键，可重复读是在事务开始的时候生成一个当前事务全局性的快照，而读提交则是每次执行语句的时候都重新生成一次快照

## RR 和 RC 的區別
想要搞清楚這個問題，我們需要先弄清楚 RR 和 RC 的區別，分析下各自的優缺點。

一致性讀
一致性讀，又稱為快照讀。快照即當前行資料之前的歷史版本。快照讀就是使用快照資訊顯示基於某個時間點的查詢結果，而不考慮與此同時執行的其他事務所執行的更改。

在MySQL 中，只有READ COMMITTED 和 REPEATABLE READ這兩種事務隔離級別才會使用一致性讀。

在 RC 中，每次讀取都會重新生成一個快照，總是讀取行的最新版本。

在 RR 中，快照會在事務中第一次SELECT語句執行時生成，只有在本事務中對資料進行更改才會更新快照。

在資料庫的 RC 這種隔離級別中，還支援"半一致讀" ，一條update語句，如果 where 條件匹配到的記錄已經加鎖，那麼InnoDB會返回記錄最近提交的版本，由MySQL上層判斷此是否需要真的加鎖。

## 解决幻读
https://www.bilibili.com/video/BV1Vt4117783?p=9&spm_id_from=pageDriver
上面介绍可重复读的时候，那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现，MySQL 已经在可重复读隔离级别下解决了幻读的问题。

前面刚说了并发写问题的解决方式就是**行锁**，而解决幻读用的也是锁，叫做**间隙锁**，MySQL 把行锁和间隙锁合并在一起，解决了并发写和幻读的问题，这个锁叫做 `Next-Key`锁。

假设现在表中有两条记录，并且 age 字段已经添加了索引，两条记录 age 的值分别为 10 和 30。

如果把事物級別用到最高 可以解決
但高併發性能很差

## 使用保留点

使用简单的 ROLLBACK 和 COMMIT 语句，就可以写入或撤销整个事务。但
是，只对简单的事务才能这样做，复杂的事务可能需要部分提交或回退。
例如前面描述的添加订单的过程就是一个事务。如果发生错误，只需要
返回到添加 Orders 行之前即可。不需要回退到 Customers 表（如果存
在的话）。
要支持回退部分事务，必须在事务处理块中的合适位置放置占位符。这
样，如果需要回退，可以回退到某个占位符

在 SQL 中，这些占位符称为保留点。在 MariaDB、MySQL 和 Oracle 中
创建占位符，可使用 SAVEPOINT 语句

![](https://i.imgur.com/X260fz1.png)


## 結論
首先说读未提交，它是性能最好，也可以说它是最野蛮的方式，因为它压根儿就不加锁，所以根本谈不上什么隔离效果，可以理解为没有隔离。

再来说串行化。读的时候加共享锁，也就是其他事务可以并发读，但是不能写。写的时候加排它锁，其他事务不能并发写也不能并发读。

最后说读提交和可重复读。这两种隔离级别是比较复杂的，既要允许一定的并发，又想要兼顾的解决问题。

## 并发写问题
**加锁的过程要分有索引和无索引两种情况**


`update user set age=11 where id = 1`
id 是这张表的主键，是有索引的情况，那么 MySQL 直接就在索引数中找到了这行数据，然后干净利落的加上行锁就可以了。

而下面这条语句

`update user set age=11 where age=10`
表中并没有为 age 字段设置索引，所以， MySQL 无法直接定位到这行数据。那怎么办呢，当然也不是加表锁了。MySQL 会为这张表中所有行加行锁，没错，是所有行。但是呢，在加上行锁后，MySQL 会进行一遍过滤，发现不满足的行就释放锁，最终只留下符合条件的行。虽然最终只为符合条件的行加了锁，但是这一锁一释放的过程对性能也是影响极大的。所以，如果是大表的话，建议合理设计索引，如果真的出现这种情况，那很难保证并发度。

## 总结
MySQL 的 InnoDB 引擎才支持事务，其中可重复读是默认的隔离级别。

读未提交和串行化基本上是不需要考虑的隔离级别，前者不加锁限制，后者相当于单线程执行，效率太差。

读提交解决了脏读问题，行锁解决了并发更新的问题。并且 MySQL 在可重复读级别解决了幻读问题，是通过行锁和间隙锁的组合 Next-Key 锁实现的。
###### tags: `MySql`