事务隔离级别不是单纯的概念题,它直接决定了并发事务在读写同一批数据时能看到什么、会不会读到中间状态。MySQL 的 InnoDB 存储引擎实现了四种隔离级别,不同的级别在一致性、锁冲突和吞吐量之间做出了不同取舍。要理解它,最好的方式不是背诵定义,而是把两个会话放到同样的表上,观察同一条 SQL 在不同隔离级别下的返回结果。

一、并发事务带来的三个经典问题
事务需要满足 ACID 特性,其中隔离性专门处理多个事务同时执行时的相互影响。如果完全不加以控制,就可能出现脏读、不可重复读和幻读。脏读是指一个事务读到了另一个事务尚未提交的修改,一旦对方回滚,读到的数据就是无效的。不可重复读是指在同一个事务内,两次读取同一行数据得到不同结果,原因是其他事务在两次读取之间提交了修改。幻读则与范围查询有关,同一个事务内用相同条件查询,第二次结果集多出或少了某些行,通常是因为其他事务插入了满足条件的新记录或删除了已有记录。
这三种问题并不是所有业务都不可接受。例如一些实时统计场景可能允许短暂读到未提交数据,但资金账户系统通常连脏读都不能出现。事务隔离级别的意义就在于给开发者一个可选范围,在不同业务中根据一致性要求选择合适级别,而不是一律使用最高或最低配置。
二、MySQL 四种隔离级别与查看方式
SQL 标准定义了四种事务隔离级别,MySQL InnoDB 全部支持。读未提交级别最低,允许事务读取其他事务未提交的数据;读已提交只允许读取已提交数据,可以避免脏读;可重复读保证同一个事务反复读取同一行时结果一致,但不解决标准的幻读问题;串行化级别最高,事务之间基本串行执行,通过锁强制避免所有并发异常。InnoDB 默认隔离级别是可重复读,这一点与 Oracle 默认的读已提交不同,从 Oracle 迁移到 MySQL 的团队尤其要注意。
InnoDB 实现隔离级别并不单纯依赖行锁,还大量使用 MVCC 多版本并发控制。每个事务在开始时会生成一个一致性快照,普通 SELECT 语句读取快照数据,因此不会阻塞写操作,写操作也不会阻塞普通读。只有更新、删除当前数据时才会涉及行锁和间隙锁。对于可重复读,InnoDB 通过间隙锁在很大程度上也避免了幻读,所以它的可重复读比 SQL 标准中的可重复读更严格一些。
下面的 SQL 可以查看当前会话和全局隔离级别,并在会话级别切换:
-- 查看当前会话的事务隔离级别 SELECT @@transaction_isolation; -- 查看全局默认隔离级别 SELECT @@global.transaction_isolation; -- 设置当前会话隔离级别为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
这里的 transaction_isolation 适用于较新的 MySQL 版本,老版本中对应变量是 tx_isolation。设置会话级别后,只对当前连接生效,新的连接仍会继承全局默认值。
三、用同一张表观察隔离级别差异
先创建一张简单的账户表,方便后续两个会话模拟并发操作:
CREATE TABLE account ( id INT PRIMARY KEY, balance DECIMAL(10,2) ); INSERT INTO account VALUES (1, 100.00);
1. 读未提交:看到未提交的修改
会话 A 将隔离级别设置为读未提交,开启事务后把余额改为 200,但不提交。此时会话 B 在相同级别下查询,能够直接读到 200。随后会话 A 执行回滚,余额恢复为 100,会话 B 之前读到的 200 就成为脏数据。这个现象就是脏读。
-- 会话 A SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; UPDATE account SET balance = 200 WHERE id = 1; -- 此时还未提交 -- 会话 B SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT balance FROM account WHERE id = 1; -- 返回 200 -- 会话 A 回滚 ROLLBACK; -- 会话 B 再次查询 SELECT balance FROM account WHERE id = 1; -- 返回 100,之前的 200 是脏数据
读未提交几乎无法用于严肃业务,它的主要问题是让事务看到了不该看到的中间状态。即使后续没有回滚,这种读也可能因为更新顺序混乱产生错误判断。唯一优势是读取开销小、阻塞少,但代价过高。
2. 读已提交:避免脏读,但不可重复读仍存在
将两个会话设置为读已提交。会话 A 第一次查询余额为 100,会话 B 随后把余额改成 200 并提交。会话 A 再次执行相同查询时得到 200。虽然第二次读到的 200 是已提交数据,不是脏数据,但在同一个事务内两次读取结果不同,这就是不可重复读。
-- 会话 A SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id = 1; -- 第一次得到 100 -- 会话 B 更新并提交 START TRANSACTION; UPDATE account SET balance = 200 WHERE id = 1; COMMIT; -- 会话 A 再次读取 SELECT balance FROM account WHERE id = 1; -- 第二次得到 200,发生不可重复读 COMMIT;
读已提交是很多关系型数据库的默认级别,它通过只读取已提交数据来避免脏读。对于某些以短事务为主、业务逻辑能容忍前后读取差异的场景,这个级别能减少长事务持有锁的时间。但如果事务内有先读后判断再写的逻辑,不可重复读可能导致判断条件失效。
3. 可重复读:事务内读取保持一致
在可重复读级别下,会话 A 第一次读取余额为 100,会话 B 即使提交了把余额改为 300 的操作,会话 A 再次读取仍然是 100。这是因为会话 A 使用了事务开始时的一致性快照,普通的 SELECT 读取的是快照版本,而不是最新提交版本。
-- 会话 A SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT balance FROM account WHERE id = 1; -- 第一次得到 100 -- 会话 B 更新并提交 START TRANSACTION; UPDATE account SET balance = 300 WHERE id = 1; COMMIT; -- 会话 A 再次读取 SELECT balance FROM account WHERE id = 1; -- 仍然得到 100,表现稳定 COMMIT;
需要注意的是,可重复读只对快照读生效。如果事务中执行 SELECT ... FOR UPDATE 或 UPDATE 这类当前读,读取的仍然是最新提交数据。InnoDB 通过行锁和间隙锁保护当前读的范围,这也是它能在可重复读级别下避免大部分幻读的原因。
4. 串行化:读操作也会加锁
串行化是最严格的隔离级别。事务中的普通 SELECT 也会对读取范围加共享锁,其他事务无法修改这些数据,直到当前事务提交或回滚。它基本消除了三种并发异常,但并发能力大幅下降,容易出现锁等待和超时。
-- 会话 A SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; SELECT balance FROM account WHERE id = 1; -- 该查询会加共享锁 -- 会话 B 尝试修改同一行,会被阻塞 UPDATE account SET balance = 400 WHERE id = 1; -- 等待会话 A 提交后才能执行 -- 会话 A 提交后,会话 B 继续执行 COMMIT;
四、工程中如何选择隔离级别
选择隔离级别没有绝对标准,核心是衡量业务能接受多大程度的数据一致性偏差,以及系统能承受多大的锁冲突。MySQL InnoDB 的默认可重复读对大多数业务是稳妥选择,它既能避免脏读和不可重复读,又通过 MVCC 和间隙锁控制了一部分幻读问题。对于读多写少、事务时间较长的应用,可重复读让普通读不阻塞写,表现较好。
如果系统并发压力大,大量事务只做简单更新和读取,并且可以将判断逻辑放到提交后再次校验或使用乐观锁解决不可重复读,那么读已提交也是一种常见选择。读已提交可以减少间隙锁的使用,降低死锁概率。读未提交基本只适合一些不重要的统计或日志类读场景。串行化适合一致性要求极高、数据量不大、并发很低的情况,因为它靠牺牲并发换取最强隔离。
无论选择哪种级别,都要确保事务尽可能短小、更新条件尽量走索引、避免在事务中执行长时间的外部调用。隔离级别解决了部分并发问题,但不能替代良好的表结构设计和索引设计。实际业务中如果遇到诡异的读写不一致,第一步不是直接调高隔离级别,而是先明确当前会话和全局隔离级别设置,再结合具体 SQL 和提交时序复现问题。
- 保留默认可重复读,适合多数在线交易场景。
- 读已提交适合更高并发且能接受逻辑校验补偿的系统。
- 读未提交不要用于核心业务。
- 串行化只在强制一致性要求下使用,并做好锁等待监控。
MySQL事务隔离级别脏读不可重复读修改时间:2026-10-06 23:14:49