MySQL事务隔离级别详解与实践

MySQL默认用可重复读隔离级别,但你真的理解四种隔离级别的区别吗?本文从脏读、不可重复读、幻读三大问题出发,详解读未提交、读已提交、可重复读、串行化的特点和适用场景。

前言

最近项目遇到了数据不一致的问题,排查下来发现是事务隔离级别设置不当导致的。

我们都知道 MySQL 默认用的是可重复读(REPEATABLE READ),但具体什么情况下会出问题?其他三种隔离级别又是怎么回事?

咱们一起来梳理一下 MySQL 的四种事务隔离级别,看看它们都解决了什么问题,又带来了什么代价。

事务并发问题

在讲隔离级别之前,我们先了解一下多事务并发时可能出现的三大问题。

脏读(Dirty Read)

事务 A 读到了事务 B 还没提交的数据。

举个例子:

-- 事务A
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 还没提交

-- 事务B(同时进行)
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 读到了500,但事务A可能回滚
COMMIT;

-- 事务A
ROLLBACK;  -- 回滚了,实际balance还是原来的值
​

事务 B 读到了一个"脏"数据,这个数据可能根本不会存在。

不可重复读(Non-Repeatable Read)

同一个事务内,多次读取同一条记录的结果不一致。

-- 事务A
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 第一次读,假设是1000

-- 事务B(同时进行)
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;  -- 提交了修改

-- 事务A继续
SELECT balance FROM account WHERE id = 1;  -- 第二次读,变成了500
COMMIT;
​

同一个事务内两次读取结果不一样,这就是不可重复读。

幻读(Phantom Read)

同一个事务内,多次执行相同的查询条件,返回的记录数量不一致。

-- 事务A
BEGIN;
SELECT COUNT(*) FROM account WHERE balance > 1000;  -- 假设返回5条

-- 事务B(同时进行)
BEGIN;
INSERT INTO account (balance) VALUES (1500);  -- 插入一条新记录
COMMIT;

-- 事务A继续
SELECT COUNT(*) FROM account WHERE balance > 1000;  -- 返回6条,多了一条
COMMIT;
​

就像出现了"幻影"一样,多了一条之前没有的记录。

四种隔离级别

MySQL 定义了四种事务隔离级别来解决这些问题,级别越高,并发性能越差,但数据一致性越好。

读未提交(READ UNCOMMITTED)

最低的隔离级别,允许读取尚未提交的数据变更。

特点:

  • 事务可以读取其他事务未提交的数据
  • 性能最好,但问题最多
  • 会出现脏读、不可重复读、幻读

适用场景:
说实话,基本不用。除非对数据一致性要求极低,但对性能要求极高的场景。

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 演示脏读
-- 会话1
BEGIN;
UPDATE account SET balance = 999 WHERE id = 1;

-- 会话2(能读到未提交的999)
SELECT balance FROM account WHERE id = 1;  -- 返回999

-- 会话1回滚
ROLLBACK;  -- balance实际还是原值,但会话2已经读到了脏数据
​

读已提交(READ COMMITTED)

只能读取已经提交的数据,是 Oracle 和 SQL Server 的默认级别。

特点:

  • 解决了脏读问题
  • 但仍会出现不可重复读和幻读
  • 每次读取都是最新的已提交数据

适用场景:
对实时性要求较高,可以接受同一事务内读取结果不一致的场景。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 演示不可重复读
-- 会话1
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 假设返回1000

-- 会话2
UPDATE account SET balance = 500 WHERE id = 1;  -- 修改并提交

-- 会话1再次读取
SELECT balance FROM account WHERE id = 1;  -- 返回500,结果变了
COMMIT;
​

可重复读(REPEATABLE READ)

MySQL 的默认隔离级别,保证同一事务内多次读取结果一致。

特点:

  • 解决了脏读和不可重复读
  • 通过 MVCC(多版本并发控制)实现
  • 理论上仍可能出现幻读,但 MySQL 通过间隙锁基本解决了

MVCC 原理简述:
每行数据都有版本号,事务开始时会获得一个版本号,只能读取小于等于这个版本的数据。

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 演示可重复读
-- 会话1
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 返回1000

-- 会话2
UPDATE account SET balance = 500 WHERE id = 1;  -- 修改并提交

-- 会话1再次读取
SELECT balance FROM account WHERE id = 1;  -- 仍然返回1000,保持一致
COMMIT;
​

这就是为什么 MySQL 默认用可重复读的原因:

既保证了数据一致性,又通过 MVCC 提供了不错的并发性能。

串行化(SERIALIZABLE)

最严格的隔离级别,完全串行化读写。

特点:

  • 解决所有并发问题(脏读、不可重复读、幻读)
  • 通过锁机制强制事务串行执行
  • 性能最差,基本没有并发性

适用场景:
对数据一致性要求极高,可以牺牲性能的关键业务场景。

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 这种级别下,事务基本是串行执行的
-- 读取时会加共享锁,修改时会加排他锁
BEGIN;
SELECT * FROM account WHERE balance > 1000;  -- 会锁住相关记录
-- 其他事务的相关操作会被阻塞
COMMIT;
​

实际应用建议

默认配置就够用

MySQL 默认的可重复读在大多数场景下都是合适的:

  • 解决了主要的并发问题
  • 性能表现良好
  • 符合大部分业务需求

我个人习惯保持默认设置,除非遇到特殊需求才调整。

什么时候需要调整

读已提交适用于:

  • 报表查询系统,需要实时看到最新数据
  • 对读一致性要求不高的业务场景

串行化适用于:

  • 金融系统的关键业务
  • 需要绝对数据一致性的场合
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 全局设置(重启后生效)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';

-- 会话级设置(当前连接有效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 单个事务设置
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 事务操作
COMMIT;
​

性能考量

隔离级别越高,锁的使用越多,并发性能越差:

  • 读未提交:无锁,性能最好
  • 读已提交:少量锁,性能较好
  • 可重复读:MVCC + 部分锁,性能适中
  • 串行化:大量锁,性能最差

在实际项目中,我们通常会针对不同的业务场景选择不同的隔离级别:

-- 核心业务用默认的可重复读
-- 报表查询可以用读已提交
-- 关键操作偶尔用串行化
​

小结

MySQL 的四种事务隔离级别各有特点:

读未提交:性能好但问题多,基本不用

读已提交:解决脏读,适合实时查询

可重复读:MySQL 默认,平衡了性能和一致性

串行化:最安全但性能差,关键场景使用

大部分情况下,保持 MySQL 的默认可重复读就够了。

遇到特殊需求时,记住这个原则:隔离级别越高越安全,但性能代价也越大。

根据业务特点选择合适的级别,才能在数据一致性和系统性能之间找到最佳平衡点。

更多推荐

章节目录