文章目录
- MySQL45讲 第二十讲 幻读是什么,幻读有什么问题?
- 一、幻读的定义
- 二、幻读带来的问题
- (一)语义问题
- (二)数据一致性问题
- 三、InnoDB 解决幻读的方法
- 四、总结
MySQL45讲 第二十讲 幻读是什么,幻读有什么问题?
在数据库事务处理的复杂世界里,幻读是一个不容忽视的重要概念。它不仅关乎数据的一致性,还与事务隔离性紧密相连。今天,我们就一同深入探讨幻读的奥秘,解析其定义、所引发的问题,以及 InnoDB 是如何巧妙解决这一难题的。
一、幻读的定义
幻读究竟是什么呢?简单来说,在可重复读隔离级别下,当一个事务对同一个范围进行前后两次查询时,后一次查询竟然发现了前一次查询中未曾出现的行。这就好比在一个神秘的魔法世界里,数据会 “凭空” 出现或消失,让人捉摸不透。
假设有一个名为t
的表,其结构如下:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
在这个表中,除了主键id
外,还有一个索引c
,并且已经初始化插入了 6 行数据。
现在,我们考虑这样一个事务操作序列。在事务 A 中,执行了三次查询语句select * from t where d = 5 for update
,分别标记为 Q1、Q2 和 Q3。根据事务可见性规则,这些查询使用了当前读,应该能够读到所有已提交记录的最新值。
- Q1 查询时,表中只有
id = 5
这一行满足d = 5
的条件,所以 Q1 只返回了这一行。 - 在 T2 时刻,事务 B 执行了
update t set d = 5 where id = 0
,将id = 0
这一行的d
值修改为 5。此时,事务 A 的 Q2 查询就会看到id = 0
和id = 5
这两行,因为事务 B 的修改已经提交,Q2 需要读到最新值。 - 接着,在 T4 时刻,事务 C 插入了一行
(1,1,5)
。当事务 A 执行 Q3 查询时,就会看到id = 0
、id = 1
和id = 5
这三行。
这里,Q3 读到id = 1
这一行的现象就是幻读。需要注意的是,在可重复读隔离级别下,普通查询是快照读,不会看到其他事务插入的数据,幻读仅在当前读下才会出现。而且,幻读特指新插入的行,像事务 B 的修改结果被事务 A 之后的查询用当前读看到,并不属于幻读范畴。
二、幻读带来的问题
(一)语义问题
从语义角度来看,幻读会导致事务的加锁声明失去意义。
就像事务 A 在 T1 时刻声明要锁住所有d = 5
的行,禁止其他事务进行读写操作。然而,由于幻读的存在,事务 B 可以修改id = 0
这一行的d
值为 5,事务 C 还能插入新的(1,1,5)
行,这显然破坏了事务 A 的加锁语义。
我们通过一个详细的场景来进一步说明。假设事务 B 和事务 C 在执行修改和插入操作时,还分别执行了其他相关操作:
session A | session B | session C | |
---|---|---|---|
T1 | begin; select * from t where d = 5 for update; / * Q1 * / | ||
T2 | update t set d = 5 where id = 0; update t set c = 5 where id = 0; | ||
T3 | select * from t where d = 5 for update; / * Q2 * / | ||
T4 | insert into t values(1,1,5); update t set c = 5 where id = 1; | ||
T5 | select * from t where d = 5 for update; / * Q3 * / | ||
T6 | commit; |
事务 B 的第二条语句update t set c = 5 where id = 0
,语义是修改id = 0
、d = 5
这一行的c
值为 5。但由于事务 A 在 T1 时刻只给id = 5
这一行加了行锁,没有锁住id = 0
这行,所以事务 B 在 T2 时刻可以执行这两条更新语句,这就与事务 A 中 Q1 语句要锁住所有d = 5
的行的语义相违背。同样,事务 C 对id = 1
这一行的修改也破坏了 Q1 的加锁声明。
(二)数据一致性问题
幻读还会引发数据一致性问题,这涉及到数据库内部数据状态以及数据和日志在逻辑上的一致性。
我们在事务 A 的 T1 时刻添加一个更新语句update t set d = 100 where d = 5
,然后分析整个执行序列完成后的情况。
经过 T1 时刻,id = 5
这一行变成(5,5,100)
,最终在 T6 时刻提交。T2 时刻,id = 0
这一行变为(0,5,5)
;T4 时刻,表中新增了一行(1,5,5)
。此时,数据库中的数据看起来似乎没有问题。
但是,当我们查看 binlog 中的内容时,就会发现问题。T2 时刻,事务 B 提交,写入了两条语句;T4 时刻,事务 C 提交,写入了两条语句;T6 时刻,事务 A 提交,写入了update t set d = 100 where d = 5
这条语句。按照 binlog 的执行顺序,最终id = 0
和id = 1
这两行的结果会变成(0,5,100)
和(1,5,100)
,与数据库中的实际结果不一致。
这种数据不一致的情况非常严重,它可能导致数据的错乱,影响系统的正常运行。例如,在一个电商系统中,如果出现这种数据不一致,可能会导致订单信息错误、库存数量不准确等问题,给企业带来巨大的损失。
三、InnoDB 解决幻读的方法
为了解决幻读问题,InnoDB 引入了一种新的锁机制 —— 间隙锁(Gap Lock)。间隙锁,顾名思义,就是锁住两个值之间的空隙。
以我们之前的表t
为例,初始化插入 6 个记录后,会产生 7 个间隙,分别是(-∞,0)
、(0,5)
、(5,10)
、(10,15)
、(15,20)
、(20,25)
、(25,+∞)
。当执行select * from t where d = 5 for update
时,InnoDB 不仅会给已有的 6 个记录加上行锁,还会同时给这 7 个间隙加上间隙锁,确保无法再插入新的记录。
间隙锁和行锁合称 Next - Key Lock,每个 Next - Key Lock 是前开后闭区间。例如,当使用select * from t for update
要锁住整个表所有记录时,就会形成 7 个 Next - Key Lock,分别是(-∞,0]
、(0,5]
、(5,10]
、(10,15]
、(15,20]
、(20, 25]
、(25, +supremum]
。这里的+supremum
是 InnoDB 为每个索引添加的一个不存在的最大值,用于满足前开后闭区间的定义。
间隙锁的引入虽然解决了幻读问题,但也带来了一些新的困扰。由于间隙锁会锁定更大的范围,可能会导致并发度下降,甚至引发死锁。
例如,考虑这样一个业务逻辑:任意锁住一行,如果该行不存在则插入,如果存在则更新其数据。
在并发情况下,可能会出现死锁现象。假设两个事务 A 和 B 都试图执行这个逻辑,且都要操作id = 9
这一行(假设该行初始不存在)
- 事务 A 先执行
select * from t where id = 9 for update
,由于id = 9
不存在,会加上间隙锁(5,10)
。 - 接着事务 B 也执行
select * from t where id = 9 for update
,同样加上间隙锁(5,10)
,此时间隙锁之间不冲突,事务 B 的语句可以执行成功。 - 然后事务 B 试图插入一行
(9,9,9)
,但被事务 A 的间隙锁挡住,进入等待状态。 - 而事务 A 此时也试图插入
(9,9,9)
,同样被事务 B 的间隙锁挡住,两个事务就进入了互相等待的死锁状态。
当然,InnoDB 的死锁检测机制会及时发现这种死锁关系,并让其中一个事务的插入语句报错返回,以避免系统长时间阻塞。
如果想要避免间隙锁带来的这些问题,还有一种配置选择,就是将隔离级别设置为读提交。在这种隔离级别下,就没有间隙锁了,但需要将 binlog 格式设置为 row,以解决可能出现的数据和日志不一致问题。不过,这种配置是否合理,需要根据具体的业务场景来分析。如果业务不需要可重复读的保证,读提交隔离级别下操作数据的锁范围更小,可能是一个合理的选择。但如果盲目跟风使用这种配置,而没有考虑业务实际需求,可能会在后续的运行中出现各种问题。
四、总结
-
幻读在数据库事务处理中是一个复杂而关键的问题,它对数据的一致性和事务的隔离性有着重要影响。通过本文的详细分析,我们了解到幻读的定义、产生的问题以及 InnoDB 的解决方案。
-
在实际应用中,我们需要深入理解这些概念,根据业务需求合理选择事务隔离级别和配置。如果对间隙锁等机制理解不足,可能会导致生产库上出现死锁等问题,影响系统的性能和稳定性。希望本文能够帮助读者更好地掌握幻读相关知识,在数据库设计和开发中做出明智的决策,构建出更加可靠、高效的数据库应用系统。