哈喽!👋 大家好!我是一个菜菜预备码农
MySQL的事务隔离级别
MySQL 支持的事务隔离级别包括:读未提交(Read Uncommitted) ,读已提交(Read Committed) ,可重复读(Repeatable Read) 和 串行化(Serializable) 。
- 读未提交:最低的隔离级别,事务可以读取其他事务未提交的数据,可能发生脏读。
- 读已提交:事务只能读取已提交的事务数据,避免脏读,但可能发生不可重复读。
- 可重复读(MySQL 默认隔离级别):事务多次读取相同数据时,结果是相同的,避免了脏读和不可重复读,但仍可能发生幻读。
- 串行化:最高的隔离级别,事务完全串行化,性能开销较大,但能避免所有并发问题。
虽然 可重复读 能有效避免脏读和不可重复读,但它仍然存在一个问题——幻读。虽然 MySQL 没有完全解决幻读问题,但它提供了一些措施来减轻这个问题。
什么是幻读?
幻读是指在一个事务内,两次相同的查询返回不同的结果集。问题的根源在于,在事务执行过程中,其他事务可能插入、删除或修改数据,导致第一次和第二次查询之间数据发生变化。
如何解决幻读?
MySQL 在处理幻读时有两种常见的解决方案,分别针对不同的场景。
1. 使用 MVCC 解决幻读(快照读)
MVCC(多版本并发控制)是一种通过使用事务快照来解决并发问题的方法。每个事务在开始时会获取数据库的一个快照,并且只能看到在该快照时间点已提交的数据,从而避免其他事务的插入、删除或修改影响当前事务的结果。
举个例子:
假设有两个事务,事务A和事务B。
- 事务A:查询余额大于1000的所有账户,并返回当前结果。
- 事务B:插入一个新的账户(余额为1200),使得
SELECT查询的条件发生变化。
在没有 MVCC 的情况下,事务A的查询可能会出现幻读,即事务B插入的数据可能会影响事务A的查询结果。
事务A:
sql
BEGIN;
SELECT * FROM accounts WHERE balance > 1000;
-- 假设返回了3个账户
事务B:
sql
BEGIN;
INSERT INTO accounts (account_id, balance) VALUES (new_account_id, 1200);
-- 事务B插入一条新的账户数据
COMMIT;
事务A 再次查询:
sql
SELECT * FROM accounts WHERE balance > 1000;
-- 假设查询结果变为4个账户(因为事务B插入的账户余额大于1000)
如果使用 MVCC 和 REPEATABLE READ 隔离级别,事务A的查询结果不会被事务B插入的账户影响。因为事务A会始终看到自己开始时的数据库快照,而不是事务B提交的数据。
使用 MVCC 和 REPEATABLE READ 隔离级别:
事务A:
sql
BEGIN;
SELECT * FROM accounts WHERE balance > 1000;
-- 事务A 会看到事务A开始时的数据库快照,因此返回的是事务开始时的3个账户,不受事务B插入影响
事务B:
sql
BEGIN;
INSERT INTO accounts (account_id, balance) VALUES (new_account_id, 1200);
-- 事务B 插入一条新记录
COMMIT;
即使事务B已经提交,事务A的查询结果仍然保持在其开始时的快照中,避免了幻读。
2. 使用 SELECT FOR UPDATE 解决幻读(Next-Key Lock)
另一种常见的方法是使用 SELECT FOR UPDATE,这是一种当前读,通过 next-key lock 解决幻读。next-key lock 是 记录锁 和 间隙锁 的结合,它不仅锁住已存在的记录,还锁住记录之间的间隙,从而防止其他事务插入符合查询条件的数据。
举个例子:
假设有一个数据库表,记录了商店的商品库存信息,表结构如下:
| 商品ID | 库存量 |
|---|---|
| 1 | 100 |
| 2 | 200 |
| 3 | 150 |
假设有两个事务,事务A和事务B。
- 事务A 的隔离级别是 可重复读(Repeatable Read)。
- 事务B 的隔离级别是 读已提交(Read Committed)。
事务A 执行查询:
sql
BEGIN;
SELECT 商品ID, 库存量 FROM 商品库存 WHERE 库存量 > 100;
-- 返回商品2和商品3
此时,事务A认为库存大于100的商品有2件。
事务B 执行插入操作:
sql
BEGIN;
INSERT INTO 商品库存 (商品ID, 库存量) VALUES (4, 120);
-- 插入商品4(库存量大于100)
COMMIT;
事务A 再次执行查询:
sql
SELECT 商品ID, 库存量 FROM 商品库存 WHERE 库存量 > 100;
-- 现在查询结果包括商品4,出现幻读
如何通过 next-key lock 解决:
next-key lock 结合了记录锁和间隙锁。在事务A执行查询时,系统不仅会锁住查询结果中的记录,还会锁住这些记录之间的“间隙”,从而防止其他事务在这些间隙中插入符合查询条件的新记录。
- 事务A会锁住商品ID=2(记录锁)。
- 事务A会锁住商品ID=3(记录锁)。
- 事务A还会锁住商品ID=1和商品ID=2之间的间隙(间隙锁)。
- 事务A还会锁住商品ID=2和商品ID=3之间的间隙(间隙锁)。
在此情况下,事务B尝试插入的商品4的库存量为120,它满足查询条件,但由于它插入的位置被锁定,事务B的插入操作将被阻塞,直到事务A完成并释放锁。事务A始终会看到相同的数据集,从而避免幻读。
总结
- MVCC 通过为每个事务创建独立的快照,解决了由于其他事务插入新数据导致的幻读问题。
next-key lock通过记录锁和间隙锁的结合,锁住查询到的数据及其周围的间隙,防止其他事务插入符合条件的数据,解决了幻读问题。间隙实际上指的是索引树中的间隙,而不是实际数据的间隙
这两种方法分别适用于不同的场景,使用 MVCC 可以通过事务的快照来避免幻读,而 next-key lock 则是通过锁住记录及间隙来解决幻读。