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 的默认可重复读就够了。
遇到特殊需求时,记住这个原则:隔离级别越高越安全,但性能代价也越大。
根据业务特点选择合适的级别,才能在数据一致性和系统性能之间找到最佳平衡点。
