在使用数据库开发实际项目时,我们经常会遇到多个事务并发执行的情况。事务一方面可以保障数据的一致性,但另一方面也可能因为并发带来一些“隐藏的问题”。为了保障数据的正确性,MySQL 提供了不同的事务隔离级别来应对这些问题。
一、并发事务带来的四大问题
在多个事务并发执行时,如果没有合适的控制机制,可能出现以下几种数据一致性问题:
问题名称 | 描述 | 示例 |
---|---|---|
脏读 | 一个事务读到了另一个未提交事务的数据。 | 事务 A 更新账户余额为 1500,但尚未提交。事务 B 查询该账户余额,读取到了 A 的未提交值 1500。若 A 回滚,B 实际读取了不存在的数据。 |
不可重复读 | 在一个事务中多次读取同一条记录时,结果却不同。 | 事务 A 开始并读取账户余额为 2000;此时事务 B 修改该余额为 1000并提交;事务 A 再次读取该余额,结果变成了 1000。两次读取结果不一致,产生不可重复读。 |
幻读 | 在一个事务中多次执行相同的范围查询,返回的记录数却不同。 | 事务 A 查询 WHERE id > 10 的记录数为 5;事务 B 在该范围插入了两条新记录并提交;事务 A 再次执行同样的查询,记录数变为 7。查询结果“发生幻觉”,称为幻读。 |
丢失更新 | 两个事务并发修改同一行,其中一个事务的修改被覆盖。 | 事务 A 将库存从 100 减到 90,事务 B 同时也从 100 减到 95。两个事务都基于初始值 100修改,最终只保留了 B 的修改,A 的更新被覆盖,造成数据丢失。 |
二、事务的四种隔离级别
MySQL 提供了四种事务隔离级别,隔离性从低到高分别是:
隔离级别 | 可避免的问题 | 是否允许脏读 | 是否允许不可重复读 | 是否允许幻读 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 无任何并发控制 | ✅允许 | ✅允许 | ✅允许 |
读已提交(Read Committed) | 避免脏读 | ❌不允许 | ✅允许 | ✅允许 |
可重复读(Repeatable Read) | 避免脏读、不可重复读(MySQL默认) | ❌不允许 | ❌不允许 | ✅允许 |
串行化(Serializable) | 完全避免三大并发问题 | ❌不允许 | ❌不允许 | ❌不允许 |
✅ = 问题可能出现;❌ = 问题被隔离/避免
三、深入理解各隔离级别的机制
-- 数据准备
create table account(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
money int comment '余额'
) comment '账户表';
insert into account(id, name, money) VALUES (null,'张三',2000),(null,'李四',2000);
1️⃣ Read Uncommitted(读未提交)
-
可以读到其他事务未提交的数据
-
存在脏读、不可重复读、幻读
-
一般不建议使用
脏读现象:
举例:
-- 设置事务隔离级别为: read uncommitted
set session transaction isolation level read uncommitted ;
事务A:
start transaction ; //开启事务
select * from account; //查询表全部数据
与此同时,开启事务B:
事务B:
-- 开启事务
start transaction ;
-- 将张三账户余额-500
update account set money = money - 500 where name = '张三';
-- 注意,此时事务B并没有commit提交事务
事务A:
-- 再次执行查询表全部数据
select * from account;
问题:
事务 A 读取到了事务 B 未提交的数据,即发生了脏读(Dirty Read)
2️⃣ Read Committed(读已提交)
-
每次查询都读取最新已提交数据
-
可以解决脏读问题
-
仍可能出现不可重复读和幻读
-
Oracle 默认使用该级别
解决了脏读:
举例:
-- 设置事务隔离级别为: read committed
set session transaction isolation level read committed ;
事务A:
start transaction ; //开启事务
select * from account; //查询表全部数据
与此同时,开启事务B:
事务B:
-- 开启事务
start transaction ;
-- 将张三账户余额-500
update account set money = money - 500 where name = '张三';
-- 注意,此时事务B并没有commit提交事务
事务A:
-- 再次执行查询表全部数据
select * from account;
结果显示:
事务A未读取到事务B还未提交的事务
事务B
-- 此时,提交事务B
commit;
结论:
事务A只能读取事务B已经提交的事务,无法读取事务B未提交的事务,没有出现脏读的现象。
Read Committed 依然存在不可重复读现象:
在该隔离级别下,虽然事务只能读取到已提交的数据,从而有效避免了脏读,但由于每次 SELECT
查询都会读取当前最新的已提交数据,因此在同一事务中多次读取同一条记录时,若其他事务在期间对该记录进行了修改并提交,当前事务将读到不同的结果。
事务A在执行查询语句的过程中,先后执行了两次:
select * from account;
却出现不一样的结果。
因此,Read Committed 虽然能防止读取未提交的数据,但不能防止其他事务对已读取数据的修改,仍然无法保障读取结果的一致性。
3️⃣ Repeatable Read(可重复读):MySQL默认
-
保证一个事务中多次读取相同行的值一致
-
避免了:
-
脏读
-
不可重复读
-
-
但仍可能出现幻读 ❗(多个记录的变化)
解决了不可重复读 :
举例:
-- 设置事务隔离级别为: repeatable read
set session transaction isolation level repeatable read ;
事务A:
start transaction ; //开启事务
select * from account; //查询表全部数据
事务B:
-- 开启事务
start transaction ;
-- 将张三账户余额-500
update account set money = money - 500 where name = '张三';
-- 事务B提交事务
commit;
事务A:
-- 再次执行查询表全部数据
select * from account;
结论:
在可重复读(Repeatable Read)隔离级别下,同一个事务内对同一条记录的多次读取结果始终一致,即使其他事务在中途对该记录进行了修改并提交,当前事务也不会受到影响,因此避免了“不可重复读”问题。
Repeatable Read:仍然会出现幻读:
举例:
-- 设置事务隔离级别为: repeatable read
set session transaction isolation level repeatable read ;
事务A:
-- 开启事务
start transaction ;
-- 查询id为3的数据,结果显示为空
select * from account where id =3
-- 于此同时,事务B执行插入操作
insert into account(id,name,money)values (3,'王五',1500);
-- 事务B提交事务
commit;
-- 事务A查询id为3的数据,结果显示为空,便执行插入操作
insert into account(id,name,money)values (3,'赵六',1500);
-- 结果却显示id为3数据已存在
-- 事务A再次执行查询id=3的数据,结果再次为空
select * from account where id =3
现象:
事务 A 查询 id = 3
的记录,发现不存在,于是尝试插入 id = 3
的新数据,但系统提示该主键已存在。再次查询时,仍未发现该记录。这种前后查询结果与实际状态不一致的现象,即为“幻读”。其根本原因是:事务 B 在事务 A 的查询之后,插入了 id = 3
的记录并提交,而事务 A 在当前隔离级别下未能感知到该插入操作。
4️⃣ Serializable(可串行化)
-
最严格,所有事务串行执行
-
完全避免:脏读、不可重复读、幻读
-
实现方式:加锁或范围锁,或者使用乐观并发控制
-
缺点:并发性能非常差,几乎不用在高并发业务系统中
举例:
-- 设置事务隔离级别为: serializable
set session transaction isolation level serializable ;
事务A:
-- 开启事务
start transaction ;
-- 查询id为3的数据,结果显示为空
select * from account where id =3
-- 于此同时,事务B执行插入操作,发生阻断
insert into account(id,name,money)values (3,'王五',1500);
-- 事务A查询id为3的数据,结果显示为空,便执行插入操作
insert into account(id,name,money)values (3,'赵六',1500);
--插入成功!
-- 事务A提交事务
commit;
-- 事务A提交事务后,事务B执行,出现报错:id为3数据已经存在
结论:
在串行化隔离级别下,所有事务串行执行,完全避免了脏读、不可重复读和幻读问题。多个事务在访问相同数据时会被强制排队等待,确保数据读取的一致性和稳定性。
虽然隔离效果最好,但由于加锁粒度更大(通常是表级锁),并发性能最低,适用于对数据一致性要求极高、并发不高的场景。
四、实战建议
场景 | 建议隔离级别 | 原因 |
---|---|---|
高并发读多写少的系统 | Repeatable Read(默认) | 一致性强,性能平衡 |
写多读少但对一致性要求不高 | Read Committed | 可接受不可重复读,但避免脏读 |
银行、财务、交易等核心系统 | Serializable | 最安全,但需牺牲性能 |
极端高性能场景、日志系统等 | Read Uncommitted | 几乎无隔离,除非你能容忍脏数据 |
五、总结
在并发事务执行过程中,若缺乏适当的隔离机制,可能导致以下四种数据一致性问题:
-
脏读(Dirty Read):读取到了其他事务尚未提交的数据
-
不可重复读(Non-Repeatable Read):同一事务内多次读取同一记录结果不一致
-
幻读(Phantom Read):同一事务内多次执行相同范围查询,返回结果集不同
-
丢失更新(Lost Update):多个事务并发更新同一数据,某些更新被覆盖丢失
MySQL 默认采用的隔离级别是 Repeatable Read,该隔离级别通过 MVCC(多版本并发控制)机制避免了脏读和不可重复读问题,但在特定场景下仍可能出现幻读现象。
在选择事务隔离级别时,需要综合考虑系统的并发性能与数据一致性要求:
应用场景 | 推荐隔离级别 | 原因说明 |
---|---|---|
银行、支付、库存等强一致性系统 | Serializable | 提供最严格的数据隔离,完全避免三类并发问题,牺牲性能 |
大多数业务系统(如 CRM、ERP) | Repeatable Read(默认) | 通过 MVCC 平衡一致性与性能,适合读多写少场景 |
高并发、允许部分数据临时不一致 | Read Committed | 避免脏读,性能较好,Oracle 默认采用 |
日志收集、监控、缓存系统等 | Read Uncommitted | 几乎无隔离,性能最高,仅适用于数据精度要求极低的场景 |
不同隔离级别之间的选择应结合业务的可容忍风险程度与系统的吞吐能力,做到按需取舍,确保事务的可靠性与系统的可扩展性。