事务隔离级别是怎么实现的? 这是我的钱包共有 100 万元。今天我心情好我决定给你的转账 100 万最后的结果肯定是我的余额变为 0 元你的余额多了 100 万元是不是想到就很开心转账这一动作在程序里会涉及到一系列的操作假设我向你转账 100 万的过程是有下面这几个步骤组成的可以看到这个转账的过程涉及到了两次修改数据库的操作。假设在执行第三步骤之后服务器忽然掉电了就会发生一个蛋疼的事情我的账户扣了 100 万但是钱并没有到你的账户上也就是说这 100 万消失了要解决这个问题就要保证转账业务里的所有数据库的操作是不可分割的要么全部执行成功 要么全部失败不允许出现中间状态的数据。数据库中的「事务Transaction」就能达到这样的效果。我们在转账操作前先开启事务等所有数据库操作执行完成后才提交事务对于已经提交的事务来说该事务对数据库所做的修改将永久生效如果中途发生发生中断或错误那么该事务期间对数据库所做的修改将会被回滚到没执行该事务之前的状态。没错今天就来图解 MySQL 事务啦开车#事务有哪些特性事务是由 MySQL 的引擎来实现的我们常见的 InnoDB 引擎它是支持事务的。不过并不是所有的引擎都能支持事务比如 MySQL 原生的 MyISAM 引擎就不支持事务也正是这样所以大多数 MySQL 的引擎都是用 InnoDB。事务看起来感觉简单但是要实现事务必须要遵守 4 个特性分别如下原子性Atomicity一个事务中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节而且事务在执行过程中发生错误会被回滚到事务开始前的状态就像这个事务从来没有执行过一样就好比买一件商品购买成功时则给商家付了钱商品到手购买失败时则商品在商家手中消费者的钱也没花出去。一致性Consistency是指事务操作前和操作后数据满足完整性约束数据库保持一致性状态。比如用户 A 和用户 B 在银行分别有 800 元和 600 元总共 1400 元用户 A 给用户 B 转账 200 元分为两个步骤从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后最后的结果是用户 A 还有 600 元用户 B 有 800 元总共 1400 元而不会出现用户 A 扣除了 200 元但用户 B 未增加的情况该情况用户 A 和 B 均为 600 元总共 1200 元。隔离性Isolation数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致因为多个事务同时使用相同的数据时不会相互干扰每个事务都有一个完整的数据空间对其他并发事务是隔离的。也就是说消费者购买商品这个事务是不影响其他消费者购买的。持久性Durability事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。InnoDB 引擎通过什么技术来保证事务的这四个特性的呢持久性是通过 redo log 重做日志来保证的原子性是通过 undo log回滚日志 来保证的隔离性是通过 MVCC多版本并发控制 或锁机制来保证的一致性则是通过持久性原子性隔离性来保证这次将重点介绍事务的隔离性这也是面试时最常问的知识的点。为什么事务要有隔离性我们就要知道并发事务时会引发什么问题。#并行事务会引发什么问题MySQL 服务端是允许多个客户端连接的这意味着 MySQL 会出现同时处理多个事务的情况。那么在同时处理多个事务的时候就可能出现脏读dirty read、不可重复读non-repeatable read、幻读phantom read的问题。接下来通过举例子给大家说明这些问题是如何发生的。#脏读如果一个事务「读到」了另一个「未提交事务修改过的数据」就意味着发生了「脏读」现象。举个栗子。假设有 A 和 B 这两个事务同时在处理事务 A 先开始从数据库中读取小林的余额数据然后再执行更新操作如果此时事务 A 还没有提交事务而此时正好事务 B 也从数据库中读取小林的余额数据那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据即使没有提交事务。因为事务 A 是还没提交事务的也就是它随时可能发生回滚操作如果在上面这种情况事务 A 发生了回滚那么事务 B 刚才得到的数据就是过期的数据这种现象就被称为脏读。#不可重复读在一个事务内多次读取同一个数据如果出现前后两次读到的数据不一样的情况就意味着发生了「不可重复读」现象。举个栗子。假设有 A 和 B 这两个事务同时在处理事务 A 先开始从数据库中读取小林的余额数据然后继续执行代码逻辑处理在这过程中如果事务 B 更新了这条数据并提交了事务那么当事务 A 再次读取该数据时就会发现前后两次读到的数据是不一致的这种现象就被称为不可重复读。#幻读在一个事务内多次查询某个符合查询条件的「记录数量」如果出现前后两次查询到的记录数量不一样的情况就意味着发生了「幻读」现象。举个栗子。假设有 A 和 B 这两个事务同时在处理事务 A 先开始从数据库查询账户余额大于 100 万的记录发现共有 5 条然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。接下来事务 A 插入了一条余额超过 100 万的账号并提交了事务此时数据库超过 100 万余额的账号个数就变为 6。然后事务 B 再次查询账户余额大于 100 万的记录此时查询到的记录数量有 6 条发现和前一次读到的记录数量不一样了就感觉发生了幻觉一样这种现象就被称为幻读。#事务的隔离级别有哪些前面我们提到当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象这些现象会对事务的一致性产生不同程序的影响。脏读读到其他事务未提交的数据不可重复读前后读取的数据不一致幻读前后读取的记录数量不一致。这三个现象的严重性排序如下SQL 标准提出了四种隔离级别来规避这些现象隔离级别越高性能效率就越低这四个隔离级别如下读未提交read uncommitted指一个事务还没提交时它做的变更就能被其他事务看到读提交read committed指一个事务提交之后它做的变更才能被其他事务看到可重复读repeatable read指一个事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的MySQL InnoDB 引擎的默认隔离级别串行化serializable会对记录加上读写锁在多个事务对这条记录进行读写操作时如果发生了读写冲突的时候后访问的事务必须等前一个事务执行完成才能继续执行按隔离水平高低排序如下针对不同的隔离级别并发事务时可能发生的现象也会不同。也就是说在「读未提交」隔离级别下可能发生脏读、不可重复读和幻读现象在「读提交」隔离级别下可能发生不可重复读和幻读现象但是不可能发生脏读现象在「可重复读」隔离级别下可能发生幻读现象但是不可能脏读和不可重复读现象在「串行化」隔离级别下脏读、不可重复读和幻读现象都不可能会发生。所以要解决脏读现象就要升级到「读提交」以上的隔离级别要解决不可重复读现象就要升级到「可重复读」的隔离级别而要彻底解决幻读现象需要升级到「串行化」隔离级别但并不建议这么做。之所以不建议升级到「串行化」来解决幻读主要是出于性能考虑「串行化」会对读写操作都加锁读操作加共享锁、写操作加排他锁相当于让所有事务串行执行无法并发并发性能会大幅下降很容易出现锁等待甚至死锁。而 MySQL 的 InnoDB 引擎在默认的「可重复读」隔离级别下已经能在很大程度上避免幻读通过 MVCC 解决快照读的幻读通过临键锁 next-key lock 解决当前读的幻读既保证了较好的隔离性又兼顾了并发性能所以一般不需要为了解决幻读而升级到「串行化」。不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样有的数据库只实现了其中几种隔离级别我们讨论的 MySQL 虽然支持 4 种隔离级别但是与SQL 标准中规定的各级隔离级别允许发生的现象却有些出入。MySQL 在「可重复读」隔离级别下可以很大程度上避免幻读现象的发生注意是很大程度避免并不是彻底避免所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生因为使用「串行化」隔离级别会影响性能。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」但是它很大程度上避免幻读现象并不是完全解决了详见这篇文章 (opens new window)解决的方案有两种针对快照读普通 select 语句是通过 MVCC 方式解决了幻读因为可重复读隔离级别下事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的即使中途有其他事务插入了一条数据是查询不出来这条数据的所以就很好地避免了幻读问题。针对当前读select ... for update 等语句是通过 next-key lock记录锁间隙锁方式解决了幻读因为当执行 select ... for update 语句的时候会加上 next-key lock如果有其他事务在 next-key lock 锁范围内插入了一条记录那么这个插入语句就会被阻塞无法成功插入所以就很好地避免了幻读问题。接下来举个具体的例子来说明这四种隔离级别有一张账户余额表里面有一条账户余额为 100 万的记录。然后有两个并发的事务事务 A 只负责查询余额事务 B 则会将我的余额改成 200 万下面是按照时间顺序执行两个事务的行为在不同隔离级别下事务 A 执行过程中查询到的余额可能会不同在「读未提交」隔离级别下事务 B 修改余额后虽然没有提交事务但是此时的余额已经可以被事务 A 看见了于是事务 A 中余额 V1 查询的值是 200 万余额 V2、V3 自然也是 200 万了在「读提交」隔离级别下事务 B 修改余额后因为没有提交事务所以事务 A 中余额 V1 的值还是 100 万等事务 B 提交完后最新的余额数据才能被事务 A 看见因此额 V2、V3 都是 200 万在「可重复读」隔离级别下事务 A 只能看见启动事务时的数据所以余额 V1、余额 V2 的值都是 100 万当事务 A 提交事务后就能看见最新的余额数据了所以余额 V3 的值是 200 万在「串行化」隔离级别下事务 B 在执行将余额 100 万修改为 200 万时由于此前事务 A 执行了读操作这样就发生了读写冲突于是就会被锁住直到事务 A 提交后事务 B 才可以继续执行所以从 A 的角度看余额 V1、V2 的值是 100 万余额 V3 的值是 200万。这四种隔离级别具体是如何实现的呢对于「读未提交」隔离级别的事务来说因为可以读到未提交事务修改的数据所以直接读取最新的数据就好了对于「串行化」隔离级别的事务来说通过加读写锁的方式来避免并行访问对于「读提交」和「可重复读」隔离级别的事务来说它们是通过Read View来实现的它们的区别在于创建 Read View 的时机不同大家可以把 Read View 理解成一个数据快照就像相机拍照那样定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View而「可重复读」隔离级别是「启动事务时」生成一个 Read View然后整个事务期间都在用这个 Read View。注意执行「开始事务」命令并不意味着启动了事务。在 MySQL 有两种开启事务的命令分别是第一种begin/start transaction 命令第二种start transaction with consistent snapshot 命令这两种开启事务的命令事务的启动时机是不同的执行了 begin/start transaction 命令后并不代表事务启动了。只有在执行这个命令后执行了第一条 select 语句才是事务真正启动的时机执行了 start transaction with consistent snapshot 命令就会马上启动事务。接下来详细说下Read View 在 MVCC 里如何工作的#Read View 在 MVCC 里如何工作的我们需要了解两个知识Read View 中四个字段作用聚簇索引记录中两个跟事务有关的隐藏列那 Read View 到底是个什么东西Read View 有四个重要的字段m_ids 指的是在创建 Read View 时当前数据库中「活跃事务」的事务 id 列表注意是一个列表“活跃事务”指的就是启动了但还没提交的事务并且不包括创建该 Read View 的事务自身。min_trx_id 指的是在创建 Read View 时当前数据库中「活跃事务」中事务id 最小的事务也就是 m_ids 的最小值。max_trx_id 这个并不是 m_ids 的最大值而是创建 Read View 时下一个将要分配给新事务的事务 id 值即全局事务 id 计数器的当前值。creator_trx_id 指的是创建该 Read View 的事务的事务 id。知道了 Read View 的字段我们还需要了解聚簇索引记录中的两个隐藏列。假设在账户余额表插入一条小林余额为 100 万的记录然后我把这两个隐藏列也画出来该记录的整个示意图如下对于使用 InnoDB 存储引擎的数据库表它的聚簇索引记录中都包含下面两个隐藏列trx_id当一个事务对某条聚簇索引记录进行改动时就会把该事务的事务 id 记录在 trx_id 隐藏列里roll_pointer每次对某条聚簇索引记录进行改动时都会把旧版本的记录写入到 undo 日志中然后这个隐藏列是个指针指向每一个旧版本记录于是就可以通过它找到修改前的记录。在创建 Read View 后我们可以将记录中的 trx_id 划分这三种情况一个事务去访问记录的时候除了「自己的更新记录总是可见(即trx_id creator_trx_id时可见)」之外还有以下几种情况:如果记录的 trx_id 值小于Read View 中的min_trx_id值表示这个版本的记录是由创建 Read View 之前就已经提交的事务生成的所以该版本对当前事务可见。如果记录的 trx_id 值大于等于Read View 中的max_trx_id值表示这个版本的记录是由创建 Read View 之后才启动的事务生成的所以该版本对当前事务不可见。如果记录的 trx_id 值介于min_trx_id和max_trx_id之间(即min_trx_id trx_id max_trx_id)说明该事务的 id 在 Read View 创建之前就已经分配出去了此时需要进一步判断 trx_id 是否在m_ids列表中:如果 trx_id在m_ids列表中表示生成该版本记录的事务在创建 Read View 的那一刻仍然活跃(尚未提交)所以该版本对当前事务不可见。如果 trx_id不在m_ids列表中表示该事务虽然 id 已分配但在创建 Read View 时已经提交所以该版本对当前事务可见。这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC多版本并发控制。上面这套可见性规则在 MySQL 8.0 InnoDB 的源码里有直接对应的实现。判断逻辑位于storage/innobase/include/read0read.h中的ReadView::changes_visible()方法/** Check whether the changes by id are visible. param[in] id transaction id to check against the view param[in] name table name return whether the view sees the modifications of id. */ bool changes_visible(trx_id_t id const table_name_t name) const MY_ATTRIBUTE((warn_unused_result)) { ut_ad(id 0); // ① 小于低水位 或 是自己的事务 → 可见 if (id m_up_limit_id || id m_creator_trx_id) { return (true); } check_trx_id_sanity(id name); // ② 大于等于高水位 → 不可见(Read View 创建之后才启动的事务) if (id m_low_limit_id) { return (false); } // ③ 活跃事务列表为空 → 可见 else if (m_ids.empty()) { return (true); } // ④ 落在 [m_up_limit_id m_low_limit_id) 区间内二分查找 // 在 m_ids 中 → 不可见;不在 → 可见 const ids_t::value_type *p m_ids.data(); return (!std::binary_search(p p m_ids.size() id)); }把源码分支和上面的规则一一对应起来:规则描述对应源码分支trx_id creator_trx_id→ 可见id m_creator_trx_id→return truetrx_id min_trx_id→ 可见id m_up_limit_id→return truetrx_id max_trx_id→ 不可见id m_low_limit_id→return false在区间内且在 m_ids 中 → 不可见binary_search(...)命中 →return false在区间内但不在 m_ids 中 → 可见binary_search(...)未命中 →return true几个值得注意的细节:关于字段命名。源码里的字段名和我们日常讲解时用的名字略有不同对应关系是:m_up_limit_id min_trx_id(低水位)m_low_limit_id max_trx_id(高水位)m_creator_trx_id creator_trx_id。up / low limit 描述的是事务 id 在数轴上的上下边界而不是数值的大小容易和直觉反着来。关于自己的事务可见。源码里第一个 if 把id m_creator_trx_id和id m_up_limit_id写在了同一行用||短路返回这意味着即使 creator_trx_id 因为某些实现原因出现在 m_ids 里也会在这一步直接判定为可见不会走到后面的二分查找。这也解释了为什么我们在概念上可以说m_ids 不包括创建该 Read View 的事务自身不管物理上包不包括行为上都等价于不包括。关于第 ③ 个分支(m_ids.empty())。这其实是一个防御性判断。因为m_up_limit_id m_ids.front()当 m_ids 为空时会被赋值为m_low_limit_id此时[m_up_limit_id m_low_limit_id)这个区间本身就是空的逻辑上根本走不到第 ④ 步。但源码还是显式写了这个判断以防极端情况下的空指针访问。关于 O(log N) 的查找效率。第 ④ 步用的是std::binary_search前提是m_ids是有序的。InnoDB 在维护trx_sys-rw_trx_ids时就保证了有序性拷贝到 m_ids 后无需再排序。这样即使系统里有成千上万个活跃事务判断单条记录可见性也只需要 O(log N) 次比较这是 MVCC 能在高并发下保持性能的关键设计之一。#可重复读是如何工作的可重复读隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View。#启动事务假设在此之前已经有一个事务 id 为 50 的事务完成了对记录的插入并提交。随后事务 A(事务 id 为 51)启动紧接着事务 B(事务 id 为 52)也启动了那这两个事务创建的 Read View 如下启动事务 A 时创建的 Read View(事务 id 为 51)creator_trx_id 51 m_ids [] ← 空列表(A 是当前唯一的活跃事务且不含自身) min_trx_id 52 ← m_ids 为空时等于 max_trx_id max_trx_id 52 ← 下一个将要分配的事务 id启动事务 B 时创建的 Read View(事务 id 为 52)creator_trx_id 52 m_ids [51] ← 只包含 A不含 B 自身 min_trx_id 51 ← m_ids 的最小值 max_trx_id 53 ← 下一个将要分配的事务 id事务 A 和事务 B 的 Read View 具体内容如下:在事务 A 的 Read View中它的creator_trx_id是 51。由于 m_ids 不包含当前事务自身而此时系统中除了 A 之外没有其他活跃事务所以 m_ids 为空列表。当 m_ids 为空时min_trx_id取max_trx_id的值即 52;而max_trx_id(下一个将要分配的事务 id)也是 52。在事务 B 的 Read View中它的creator_trx_id是 52。由于事务 A 处于活跃状态且 m_ids 不包括事务 B 自身所以此时 m_ids 只包含事务 A 的 id 51;min_trx_id为 m_ids 的最小值即 51;max_trx_id(下一个将要分配的事务 id)为 53。接着在可重复读隔离级别下事务 A 和事务 B 按顺序执行了以下操作事务 B 读取小林的账户余额记录读到余额是 100 万事务 A 将小林的账户余额记录修改成 200 万并没有提交事务事务 B 读取小林的账户余额记录读到余额还是 100 万事务 A 提交事务事务 B 读取小林的账户余额记录读到余额依然还是 100 万接下来跟大家具体分析下。#第一次读事务 B 首次读取记录事务 B 第一次读小林的账户余额记录在找到记录后它会先看这条记录的 trx_id。此时发现 trx_id 为 50比事务 B 的 Read View 中的 min_trx_id 值(51)还小根据可见性规则这意味着修改这条记录的事务早就在事务 B 启动前提交过了所以该版本的记录对事务 B可见也就是事务 B 可以读取到这条记录余额为 100 万。#事务 A 修改记录形成版本链接着事务 A 通过 update 语句将这条记录修改了(还未提交事务)将小林的余额改成 200 万。这时 MySQL 会记录相应的 undo log并以链表的方式串联起来形成版本链如下图:从记录的字段可以看到由于事务 A 修改了该记录以前的记录就变成旧版本记录了最新记录和旧版本记录通过 roll_pointer 以链表的方式串起来而且最新记录的 trx_id 是事务 A 的事务 id(trx_id 51)。#第二次读事务 B 再次读取记录(事务 A 未提交)然后事务 B 第二次去读取该记录发现这条记录的trx_id 值为 51。对照事务 B 的 Read View:trx_id(51)不小于 min_trx_id(51)不满足直接可见的条件;trx_id(51)也小于 max_trx_id(53)落在[min_trx_id max_trx_id)区间内;此时需要判断 trx_id 是否在m_ids [51]中——结果是在的。这说明这条记录是被事务 B 的 Read View 创建时仍然活跃的事务(也就是事务 A)修改的这个版本对事务 B不可见。于是事务 B 沿着roll_pointer指向的 undo log 链条往下找旧版本记录直到找到trx_id 严格小于事务 B 的 Read View 中 min_trx_id 值的第一条记录。沿着链条往下一条是 trx_id 50 的旧版本满足 50 min_trx_id(51)所以事务 B 能读取到的是这条旧版本也就是小林余额为 100 万的记录。#第三次读事务 B 再次读取记录(事务 A 已提交)最后当事务 A 提交事务后由于隔离级别是「可重复读」所以事务 B 再次读取记录时依然使用启动事务时创建的那个 Read View来判断当前版本的记录是否可见。这个 Read View 在事务 B 的整个生命周期内都不会变。因此即使事务 A 已经把小林余额改成 200 万并提交事务 B 的 Read View 里 m_ids 依然记录着[51]这条最新版本的 trx_id 51 依旧会被判为不可见依然会沿着 undo log 往下读到 trx_id 50 的旧版本。所以事务 B 第三次读取记录时读到的仍然是小林余额为 100 万的这条记录。就是通过这样的方式实现了在「可重复读」隔离级别下事务期间读到的记录始终是事务启动前已提交的版本。#读提交是如何工作的读提交隔离级别是在每次读取数据时都会生成一个新的 Read View。也意味着事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。那读提交隔离级别是怎么工作呢我们还是以前面的例子来聊聊。假设事务 A 事务 id 为51启动后紧接着事务 B 事务 id 为52也启动了接着按顺序执行了以下操作事务 B 读取数据创建 Read View小林的账户余额为 100 万事务 A 修改数据还没提交事务将小林的账户余额从 100 万修改成了 200 万事务 B 读取数据创建 Read View小林的账户余额为 100 万事务 A 提交事务事务 B 读取数据创建 Read View小林的账户余额为 200 万那具体怎么做到的呢?我们重点看事务 B 每次读取数据时创建的 Read View。第一步事务 B 读取数据。创建的 Read View(此时 A 未提交仍活跃):第二步事务 A 修改数据(还未提交)— 形成版本链:第三步事务 B 读取数据。 创建的 Read View(此时 A 仍未提交仍活跃所以这个 Read View 和第一步的完全一样):#为什么事务 B 第二次读数据时读不到事务 A(还未提交事务)修改的数据?事务 B 在找到小林这条记录时会看这条记录的 trx_id 是 51。对照事务 B 这次新创建的 Read View:trx_id(51)不小于 min_trx_id(51)不满足直接可见;trx_id(51)小于 max_trx_id(53)落在[min_trx_id max_trx_id)区间内;需要判断 trx_id 是否在 m_ids 范围内——判断的结果是在的。那么说明这条记录是被还未提交的事务修改的这时事务 B 并不会读取这个版本的记录。于是沿着 undo log 链条往下找旧版本的记录直到找到 trx_id「小于」事务 B 的 Read View 中 min_trx_id 值的第一条记录所以事务 B 能读取到的是 trx_id 为 50 的记录也就是小林余额是 100 万的这条记录。#为什么事务 A 提交后事务 B 就可以读到事务 A 修改的数据?在事务 A 提交后由于隔离级别是「读提交」所以事务 B 在每次读数据的时候会重新创建 Read View。此时事务 A 已经提交不再是活跃事务所以不会再出现在 m_ids 里。事务 B 第三次读取数据时创建的 Read View 如下:事务 B 在找到小林这条记录时会发现这条记录的 trx_id 是 51比事务 B 的 Read View 中的 min_trx_id 值(53)还小这意味着修改这条记录的事务早就在创建 Read View 前提交过了所以该版本的记录对事务 B 是可见的。因此事务 B 能读到最新版本小林余额为 200 万。正是因为在读提交隔离级别下事务每次读数据时都重新创建 Read View那么在事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。#总结事务是在 MySQL 引擎层实现的我们常见的 InnoDB 引擎是支持事务的事务的四大特性是原子性、一致性、隔离性、持久性我们这次主要讲的是隔离性。当多个事务并发执行的时候会引发脏读、不可重复读、幻读这些问题那为了避免这些问题SQL 提出了四种隔离级别分别是读未提交、读已提交、可重复读、串行化从左往右隔离级别顺序递增隔离级别越高意味着性能越差InnoDB 引擎的默认隔离级别是可重复读。要解决脏读现象就要将隔离级别升级到读已提交以上的隔离级别要解决不可重复读现象就要将隔离级别升级到可重复读以上的隔离级别。而对于幻读现象不建议将隔离级别升级为串行化因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」但是它很大程度上避免幻读现象并不是完全解决了详见这篇文章 (opens new window)解决的方案有两种针对快照读普通 select 语句是通过 MVCC 方式解决了幻读因为可重复读隔离级别下事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的即使中途有其他事务插入了一条数据是查询不出来这条数据的所以就很好地避免了幻读问题。针对当前读select ... for update 等语句是通过 next-key lock记录锁间隙锁方式解决了幻读因为当执行 select ... for update 语句的时候会加上 next-key lock如果有其他事务在 next-key lock 锁范围内插入了一条记录那么这个插入语句就会被阻塞无法成功插入所以就很好地避免了幻读问题。对于「读提交」和「可重复读」隔离级别的事务来说它们是通过 Read View 来实现的它们的区别在于创建 Read View 的时机不同「读提交」隔离级别是在每个 select 都会生成一个新的 Read View也意味着事务期间的多次读取同一条数据前后两次读的数据可能会出现不一致因为可能这期间另外一个事务修改了该记录并提交了事务。「可重复读」隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View这样就保证了在事务期间读到的数据都是事务启动前的记录。这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对来控制并发事务访问同一个记录时的行为这就叫 MVCC多版本并发控制。在可重复读隔离级别中普通的 select 语句就是基于 MVCC 实现的快照读也就是不会加锁的。而 select .. for update 语句就不是快照读了而是当前读了也就是每次读都是拿到最新版本的数据但是它会对读到的记录加上 next-key lock 锁。