MySQL事务隔离级别详解
1. 基本概念
1.1 什么是事务隔离级别?
事务隔离级别是数据库管理系统为了保证数据一致性,在多个事务并发访问时提供的不同级别的保护机制。
1.2 事务并发问题
脏读(Dirty Read):
- 一个事务读取到另一个事务未提交的数据
- 如果那个事务回滚,读到的数据就是无效的
不可重复读(Non-repeatable Read):
- 同一事务中,多次读取同一数据得到不同结果
- 因为其他事务修改并提交了这个数据
幻读(Phantom Read):
- 同一事务中,多次查询某个范围的记录,数量不一致
- 因为其他事务插入或删除了符合这个范围的记录
2. 四种隔离级别
2.1 读未提交(READ UNCOMMITTED)
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 示例:脏读
-- 事务1
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 此时事务2可以读取到余额减少100的结果
ROLLBACK; -- 如果回滚,事务2读取到的数据就是脏数据
-- 事务2
SELECT balance FROM account WHERE id = 1; -- 读取到未提交的数据
特点:
- 最低的隔离级别
- 允许读取未提交的数据
- 可能出现脏读、不可重复读和幻读
- 性能最好,但几乎不用
2.2 读已提交(READ COMMITTED)
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 示例:不可重复读
-- 事务1
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 第一次读取:1000
-- 事务2执行更新并提交
SELECT balance FROM account WHERE id = 1; -- 第二次读取:900
COMMIT;
特点:
- Oracle默认的隔离级别
- 只能读取已经提交的数据
- 避免了脏读
- 可能出现不可重复读和幻读
2.3 可重复读(REPEATABLE READ)
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 示例:避免不可重复读
-- 事务1
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 第一次读取:1000
-- 事务2执行更新并提交
SELECT balance FROM account WHERE id = 1; -- 第二次读取:仍然是1000
COMMIT;
-- 示例:可能的幻读
-- 事务1
START TRANSACTION;
SELECT COUNT(*) FROM account WHERE balance > 1000; -- 返回10条
-- 事务2插入一条balance=1500的记录并提交
INSERT INTO account VALUES (1500); -- 这条记录可能会出现在后续的查询中
COMMIT;
特点:
- MySQL的默认隔离级别
- 同一事务中多次读取结果一致
- 避免了脏读和不可重复读
- 使用MVCC(多版本并发控制)实现
- 在某些场景下可能出现幻读
2.4 串行化(SERIALIZABLE)
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 示例:完全串行化执行
-- 事务1
START TRANSACTION;
SELECT * FROM account WHERE balance > 1000;
-- 事务2必须等待事务1完成才能执行
COMMIT;
特点:
- 最高的隔离级别
- 完全串行化执行,避免所有并发问题
- 性能最差,很少使用
3. 实现原理
3.1 MVCC(多版本并发控制)
-- MVCC相关的隐藏字段
-- DB_TRX_ID:最后一次修改该记录的事务ID
-- DB_ROLL_PTR:回滚指针,指向上一个版本
-- DB_ROW_ID:隐藏主键
3.2 锁机制
-- 共享锁(S锁)
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
-- 排他锁(X锁)
SELECT * FROM account WHERE id = 1 FOR UPDATE;
4. 最佳实践
4.1 选择合适的隔离级别
- 一般情况下使用默认的REPEATABLE READ
- 对一致性要求特别高的场景可以使用SERIALIZABLE
- 需要权衡性能和一致性
4.2 注意事项
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 开启事务
START TRANSACTION;
-- 提交或回滚
COMMIT;
ROLLBACK;
4.3 常见问题解决
性能问题:
- 避免长事务
- 合理设置隔离级别
- 适当使用锁机制
并发问题:
- 使用乐观锁或悲观锁
- 合理设计业务逻辑
- 正确处理死锁
什么是幻读?
幻读是指在同一事务中,多次查询某个范围的记录,数量不一致。因为其他事务插入或删除了符合这个范围的记录。大白话就是同一事务内执行两次相同的查询,结果不一致。
当前读场景解决?
场景1:select xx for update 其他事务不能插入数据
此时无论怎么查询都不会多出数据,因为其他事务根本没法插入了
场景2:
事务1普通select查询whereid=3
事务2插入insertid=3然后提交
事务1普通select查询whereid=3会查询不到数据
事务1如果直接updatexx whereid=3此时数据就会被更新到,再次查
为什么会被更新,因为当前读语法必须保证读到最新已经被提交的数
询就会查询出数据,产生幻读现象
那一旦更新到数据了,此时这条数据的undolog版本链最新的事务id就
是当前的事务id了
所以一旦事务1再次进行普通select查询,那这条数据就会被查询出出来
对于事务1来说就是两次查询结果不一样发生幻读
场景3:
T1时刻:事务1先执行快照读语句 select * from test where id>100;得到3条记录
T2时刻:事务2插入一个id=200的记录并提交
T3时刻:事务1再执行快照读语句 select * from test where id>100;得到4条记录
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事之后
,马上执行select…for update这类当前读的语句,因为它会对记
录加next-keylock,从而避免其他事务插入一条新记录
其实场景2和场景3本质上都是先快照读后来发生了当前读针对这种
情况mysql确实没办法,
换个角度去思考的话这种问题一定是问题嘛?其实也不一定看你如何
定义,像大厂高并发业务场景的数据库隔离级别都会调整后成rc的,大家
都认为幻读不一定是问题,需要根据业务场景自行处理
序列化 最慢的所有事务串行执行,没什么好说的