MySQL 中的事务隔离级别有哪些?默认的事务隔离级别是什么?为什么选择这个级别?
面试题考点分析
- 能否准确说出 InnoDB 支持的四种事务隔离级别,以及每种级别解决哪些并发问题。
- 是否真正理解脏读、不可重复读、幻读三个概念及其区别。
- 是否知道 MySQL 默认隔离级别是 REPEATABLE READ,并能说清楚选择它的原因。
- 是否掌握 MVCC、ReadView、Next-Key Lock 等底层机制,把「可重复读」和「避免幻读」正确关联起来。
- 能否在 Java、JDBC 或 Spring 中正确查看和配置隔离级别。
一、标准回答
MySQL 的 InnoDB 存储引擎支持四种事务隔离级别:READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重复读)和 SERIALIZABLE(串行化)。其中,默认的事务隔离级别是 REPEATABLE READ(可重复读)。
事务隔离级别的作用,是在事务并发执行时,平衡「数据一致性」与「并发性能」两个目标。隔离级别越低,事务间互相可见的数据越多,一致性越弱,但并发性能越好;隔离级别越高,数据一致性越强,但锁冲突和等待越多,吞吐量越低。
| 隔离级别 | 能否解决脏读 | 能否解决不可重复读 | 能否解决幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 否 | 否 | 否 |
| READ COMMITTED | 是 | 否 | 否 |
| REPEATABLE READ | 是 | 是 | MySQL 通过 Next-Key Lock 基本解决 |
| SERIALIZABLE | 是 | 是 | 是 |
面试时可以直接给结论:“MySQL InnoDB 默认使用可重复读,而不是 SQL 标准推荐的读已提交。它通过 MVCC 保证同一事务内多次读取结果一致,并借助 Next-Key Lock 基本解决幻读,因此在安全和并发性能之间取得了较好的平衡。”这句话既说明了作用,也点出了 MySQL 和 SQL 标准的差异,是面试官比较认可的回答口径。
二、核心原理
2.1 并发事务引起的三大问题
理解隔离级别之前,要先理解并发事务可能带来的三类数据错误:
- 脏读:事务 A 读到了事务 B 尚未提交的数据。如果 B 之后回滚,A 读到的就是「脏数据」。READ COMMITTED 及以上级别可解决。
- 不可重复读:事务 A 内多次读取同一行,期间事务 B 修改并提交了该行,导致 A 两次读取结果不一致。REPEATABLE READ 及以上级别可解决。
- 幻读:事务 A 按某个条件查询一组行,期间事务 B 插入或删除了满足该条件的行,导致 A 再次查询时出现「凭空多出或消失的行」。通常需要 SERIALIZABLE,MySQL 在 RR 级别下通过 Next-Key Lock 基本解决。
简单记忆:脏读针对「未提交」,不可重复读针对「已提交的修改」,幻读针对「已提交的插入/删除」。
2.2 MVCC 如何实现可重复读
InnoDB 采用MVCC(多版本并发控制)实现快照读。每行记录隐藏两个系统列:DB_TRX_ID(最近修改该行的事务 ID)和DB_ROLL_PTR(指向回滚段的 undo log 指针)。事务读取时不会直接加锁,而是根据当前事务的ReadView判断某行版本是否可见:
- READ COMMITTED:每次查询都会生成新的 ReadView,因此能看到其他事务已提交的最新数据,但可能发生不可重复读。
- REPEATABLE READ:事务第一次查询时生成 ReadView,并复用至事务结束,因此同一事务内多次快照读结果一致,实现了可重复读。
可以类比为:RR 级别在事务开始时「拍一张数据快照」,事务内后续读取都基于这张快照,外部事务的提交修改对当前事务不可见。官方文档明确说明,InnoDB 通过这种机制避免了加读锁带来的性能开销。
2.3 Next-Key Lock 如何解决幻读
MVCC 只能保证快照读(普通 SELECT)的可重复读,而当前读(如SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT)需要依赖锁机制。InnoDB 的Next-Key Lock是「行锁 + 间隙锁(Gap Lock)」的组合:
- 行锁:锁定具体的已存在记录。
- 间隙锁:锁定记录之间的空隙范围,阻止其他事务在该范围内插入新记录。
例如,事务执行SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE时,不仅会锁定 id 为 10 到 20 的已存在行,还会锁定这些行之间的间隙,从而阻止其他事务在该范围内插入满足条件的新行,达到防止幻读的效果。这也是 MySQL 的 RR 级别比 SQL 标准定义的 RR「更严格」的根本原因。
三、应用场景
3.1 日常开发场景
- 默认业务场景:大多数 CRUD 业务直接使用数据库默认的 REPEATABLE READ 即可,既能保证事务内读取一致,又无需额外配置。
- 金融、账户余额类场景:涉及资金变动、余额扣减时,保持 RR 级别,并通过
SELECT ... FOR UPDATE对行加锁,避免并发扣减导致超卖或余额不一致。 - 高并发读多写少场景:如商品浏览、内容展示,如果对一致性要求不苛刻,可将隔离级别降低为 READ COMMITTED,减少间隙锁带来的锁冲突和死锁概率。
3.2 企业真实场景
- 电商下单扣库存:先查询库存,再扣减库存。若使用 READ COMMITTED,可能出现同一库存被两个事务重复扣减;使用 RR 加行锁则可避免不可重复读与并发超卖。
- 账户转账:从账户 A 扣款、向账户 B 加款必须在一个事务内完成。RR 级别配合行锁,可避免扣款成功后加款前其他事务读取到中间状态。
- 报表统计与对账:统计类 SQL 需要读取稳定的数据快照,RR 级别保证同一事务内多次聚合查询结果一致,避免统计口径漂移。
四、使用方式
4.1 JDBC 查看和设置隔离级别
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class TransactionIsolationDemo { public static void main(String[] args) throws Exception { // 1. 四种隔离级别对应的 JDBC 常量 System.out.println("READ_UNCOMMITTED = " + Connection.TRANSACTION_READ_UNCOMMITTED); System.out.println("READ_COMMITTED = " + Connection.TRANSACTION_READ_COMMITTED); System.out.println("REPEATABLE_READ = " + Connection.TRANSACTION_REPEATABLE_READ); System.out.println("SERIALIZABLE = " + Connection.TRANSACTION_SERIALIZABLE); Connection conn = getConnection(); // 2. 查看当前连接默认隔离级别(MySQL 默认输出 4,即 REPEATABLE_READ) int defaultLevel = conn.getTransactionIsolation(); System.out.println("默认隔离级别: " + defaultLevel); // 3. 显式设置为可重复读 conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 4. 关闭自动提交,开启手动事务 conn.setAutoCommit(false); try (Statement stmt = conn.createStatement()) { stmt.executeUpdate("UPDATE account SET balance = balance - 100 WHERE id = 1"); stmt.executeUpdate("UPDATE account SET balance = balance + 100 WHERE id = 2"); conn.commit(); System.out.println("事务提交成功"); } catch (SQLException e) { conn.rollback(); System.out.println("事务回滚"); throw e; } finally { conn.close(); } } private static Connection getConnection() throws SQLException { String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai"; return DriverManager.getConnection(url, "root", "123456"); } }4.2 SQL 层面查看和设置
-- 查看当前会话隔离级别 SELECT @@transaction_isolation; -- 查看全局默认隔离级别 SELECT @@global.transaction_isolation; -- 设置当前会话隔离级别为可重复读 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 设置全局默认隔离级别 SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;4.3 Spring 中的配置
import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service public class AccountService { @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public void transfer(Long fromId, Long toId, Long amount) { // 执行扣款、加款逻辑 } }4.4 执行流程与注意事项
执行流程可以概括为:获取连接 → 查看或设置隔离级别 → 关闭自动提交 → 执行一组业务 SQL → 提交或回滚 → 释放连接。
- 隔离级别要在事务开始前设置。如果事务已经开始,再修改隔离级别对当前正在进行的事务不一定生效。
- 必须通过
setAutoCommit(false)显式开启事务,否则每条 SQL 自动提交,事务边界和隔离级别就失去了意义。 - 使用连接池时要格外注意:连接归还后可能被其他线程复用,隔离级别是连接级属性,不会自动恢复默认值,建议在归还连接前将隔离级别重置为默认值。
getTransactionIsolation()返回的是 JDBC 常量值,其中TRANSACTION_REPEATABLE_READ对应数值 4,TRANSACTION_READ_COMMITTED对应数值 2,可以用来与原默认值做比对。
五、扩展延伸
5.1 与 SQL 标准及主流数据库的对比
SQL 标准定义的 REPEATABLE READ 只要求解决不可重复读,并没有强制解决幻读。MySQL 因为引入了 Next-Key Lock,在 RR 级别下也能基本避免幻读,因此比标准定义更严格。不同数据库的默认隔离级别也不同:
| 数据库 | 默认事务隔离级别 |
|---|---|
| MySQL InnoDB | REPEATABLE READ |
| Oracle | READ COMMITTED |
| PostgreSQL | READ COMMITTED |
| SQL Server | READ COMMITTED |
5.2 选择 REPEATABLE READ 的原因
- 历史兼容性:早期 MySQL 主从复制依赖 statement-based binlog,RR 级别能保证主从执行顺序一致,避免复制数据不一致。
- 一致性足够强:RR 配合 Next-Key Lock 已基本解决幻读,安全性接近 SERIALIZABLE,能满足大多数业务对数据的一致性要求。
- 并发性能更好:与 SERIALIZABLE 的完全串行化相比,RR 采用 MVCC 快照读,读操作不加锁,写操作只需要行锁和间隙锁,并发吞吐量高得多。
5.3 优缺点与实际开发注意事项
RR 的优点是一致性高、能解决脏读、不可重复读以及大部分幻读;缺点是间隙锁会扩大锁范围,降低并发度,并可能增加死锁概率。
- 尽量使用索引:间隙锁的范围与索引条件强相关,缺少合适索引时,InnoDB 可能锁住更多记录甚至更宽的范围,要保证 WHERE 条件命中索引。
- 控制事务时长:RR 的长事务会长时间持有 ReadView 和 undo log,可能导致 undo log 膨胀和锁等待,应避免在事务内做远程调用或慢查询。
- 关注死锁:间隙锁和行锁的粒度不同,多个事务交叉加锁容易死锁,可通过统一加锁顺序、缩短事务、配置死锁检测和重试来缓解。
- 高并发业务可评估降级为 RC:互联网高并发场景下,很多团队将隔离级别改为 READ COMMITTED,配合 row 格式 binlog 保证复制一致性,以换取更高的并发性能。
六、面试追问
追问 1:既然 RR 能解决幻读,那 SERIALIZABLE 还有什么用?
回答思路:先承认 MySQL 的 RR 确实能解决绝大多数幻读,再指出两者的实现机制和性能差异。
标准答案:MySQL 的 RR 通过 MVCC 解决快照读幻读,通过 Next-Key Lock 解决当前读幻读,确实覆盖了绝大多数场景。但 SERIALIZABLE 会把所有普通 SELECT 也变成加锁读(相当于隐式加共享锁),彻底串行化事务,实现理论上的完全隔离。代价是并发性能急剧下降。因此只有在一致性要求极高、可以接受较低并发的场景下才使用 SERIALIZABLE。
追问 2:快照读和当前读有什么区别?
回答思路:从是否加锁、是否走 MVCC 两个维度区分。
标准答案:普通 SELECT 属于快照读,通过 MVCC 读取 ReadView 对应的数据版本,不加锁,性能高。当前读包括SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT,读取的是数据的最新提交版本,并通过行锁或 Next-Key Lock 保证操作期间数据不被并发修改。这也是 RR 级别下,单纯 SELECT 和带 FOR UPDATE 的 SELECT 可能读到不同结果的根源。
追问 3:为什么互联网公司常常把隔离级别改成 READ COMMITTED?
回答思路:从并发性能和复制机制两个角度解释,并说明前提条件。
标准答案:READ COMMITTED 每次查询都会生成新的 ReadView,且没有间隙锁,锁范围更小,并发吞吐量更高,死锁概率也更低。早期 MySQL 依赖 statement-based binlog 必须使用 RR 保证主从一致;而现在普遍使用 row 格式 binlog,记录的是行变更结果,主从可以保持一致,因此用 RC 换取更高并发成为优化选择。这是一个典型的「一致性换性能」的工程取舍。
追问 4:如何查看和修改 MySQL 的默认隔离级别?
回答思路:分「查看」和「修改」两步,说明会话级与全局级的区别。
标准答案:查看当前会话用SELECT @@transaction_isolation;,查看全局默认值用SELECT @@global.transaction_isolation;。修改会话级用SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;,修改全局默认值用SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;。需要注意,全局修改只影响之后新建的连接,已存在的连接不会改变;也可以修改配置文件my.cnf中的transaction-isolation参数实现持久化。
