1、前言
为什么在Mysql默认REPEATABLE READ事务隔离级别下,好多人说无法避免“幻读”现象,REPEATABLE READ
难道不是采用多版本并发控制(MVCC)和间隙锁(Gap Locks)机制来实现的吗?既然是间隙锁,还会产生幻
读?难道这不是悖论吗?
如果你有这些疑问,那就花费3分钟继续往下看吧,通俗异动,看了之后会有一定的收获的…
2、什么是幻读
在一个事务中,如果两次执行同样的范围查询(如基于某个条件的查询),第二次查询返回了第一次查询时
不存在的行。这种情况就像是在第一次查询时,这些行是“隐形的”,而在第二次查询时突然“幻化”出来,故称为“幻
读”。
大白话来说,在一个事务里面有两次同样的读取操作,但是前后两次读取的结果不一致,后一次读取的数据
比前一次读取的较多,这中现象俗称为“幻读”。
3、Mysql的事务隔离级别
MySQL支持四种标准的事务隔离级别,这些级别定义了在并发环境下事务如何与其他事务交互,以及事务之
间如何看到彼此所做的更改。这四种隔离级别按照从最低到最高顺序排列如下:
-
读未提交(READ UNCOMMITTED)
在这个级别下,一个事务可以看到其他事务未提交的更改。这可能导致以下三种问题: 1、脏读(Dirty Reads):一个事务读取了另一个事务未提交的数据。 2、不可重复读(Non-repeatable Reads):在同一个事务中,两次相同的查询返回不同的结果,因为另一个事务在此期间做了更改。 3、幻读(Phantom Reads):在一个事务中,两次执行同样的范围查询,第二次查询返回了第一次查询时不存在的行。
-
读已提交(READ COMMITTED)
这个级别禁止了脏读,但仍然允许不可重复读和幻读。在一个事务中,只能读取其他事务已经提交的数据。
-
可重复读(REPEATABLE READ默认事务隔离级别)
MySQL的默认隔离级别。在这个级别下,一个事务中的所有查询都会看到在事务开始时的数据库快照。这避免了脏读和不可重复读,但对于幻读的处理取决于存储引擎的实现。InnoDB使用多版本并发控制(MVCC)和Next-Key Locking来尽量减少幻读,但在某些情况下仍然可能发生。
-
串行化(SERIALIZABLE)
这是最严格的隔离级别,它通过强制事务串行执行来避免所有并发问题,包括脏读、不可重复读和幻读。虽然这保证了数据的一致性,但也大大降低了系统的并发性能。
在实际应用中,选择合适的事务隔离级别需要在数据一致性和系统性能之间做出权衡。较低的隔离级别通
常提供更高的并发性能,而较高的隔离级别则提供更强的数据一致性保证。
4、Mysql里面的锁
MySQL中的锁机制是数据库管理系统用来控制并发事务对数据资源访问的关键技术。锁可以防止多个事务同
时修改相同的数据,从而确保数据的一致性和事务的隔离性。MySQL提供了多种类型的锁,适用于不同的存储引
擎和应用场景。以下是MySQL中几种主要的锁类型:
-
行锁(Row Locks)重点
1、行锁是最细粒度的锁,它只锁定被事务访问的具体数据行,注意是仅锁定具体“数据行本身”。 2、InnoDB存储引擎支持行锁,包括共享锁(S Lock)和排他锁(X Lock)。 3、共享锁允许多个事务同时读取同一行数据,但不允许任何事务“修改”数据。 4、排他锁允许事务独占访问一行数据,进行写操作,其他事务则不能读取或写入这行数据。
-
间隙锁(Gap Locks)重点
InnoDB在REPEATABLE READ隔离级别下使用间隙锁,用于锁定查询范围内的空隙,防止其他事务在这些空隙中插入新行,从而避免幻读。 间隙锁可以防止所谓的“幻读”现象,即在事务中两次执行相同的查询,第二次查询返回了第一次查询时不存在的行。
-
Next-Key Locks 重点
这是InnoDB中行锁和间隙锁的组合,用于REPEATABLE READ隔离级别,可以更有效地防止幻读。
-
表锁(Table Locks)
1、表锁会锁定整个表,阻止其他事务对该表进行任何读写操作。 2、MyISAM存储引擎使用表锁,这会限制并发性能,但可以简化锁管理。 3、InnoDB存储引擎在某些情况下也会使用表锁,如FLUSH TABLES WITH READ LOCK命令。
-
意向锁(Intention Locks)
意向锁是InnoDB中的一种特殊锁,用于表示事务想要在表中加锁的意图。 意向共享锁(IS Lock)表示事务想要获取表中某些行的共享锁。 意向排他锁(IX Lock)表示事务想要获取表中某些行的排他锁。
[PS:
MySQL的InnoDB存储引擎会根据查询的类型和数据分布动态调整锁的策略,以平衡并发性和一致性。
]
5、REPEATABLE READ默认事务隔离级别
在MySQL的InnoDB存储引擎中,REPEATABLE READ隔离级别确实能够防止本事务内的“不可重复读”和“幻
读”,这是通过多版本并发控制(MVCC)和间隙锁(Gap Locks)机制实现的。
-
MVCC(Multi-Version Concurrency Control)
确保事务能看到在事务开始时的快照,这防止了“不可重复读”。
-
Gap Locks[间隙锁]:
在REPEATABLE READ隔离级别下,InnoDB会使用间隙锁来锁定查询范围内的所有行,包括那些尚未存在的行。这样可以防止其他事务在已锁定的范围内插入新行,从而避免“幻读”。 但是,这里有一个关键点需要注意:在某些情况下,InnoDB为了提高并发性能,可能会选择不使用间隙锁,而是使用一种称为Next-Key Locking的组合锁策略,它结合了行锁和间隙锁。
5.1、什么是间隙锁
假设有这样一条sql,如下:
SELECT * FROM a WHERE age BETWEEN 20 AND 30 FOR UPDATE;
a表里面20-30之间的数据有20 ,23,26,30四条数据,那么间隙锁的情况是怎么样的呢?
在MySQL的InnoDB存储引擎中,对于包含数据20, 23, 26, 30
的表(假设这些数字是某个索引,比如年龄字段
上的索引值),间隙锁(Gap Locks)和Next-Key Locks的锁定情况可以如下说明:
5.1.1、索引值
- 索引值:
20, 23, 26, 30
5.1.2、间隙(Gaps)
- 间隙是指两个索引值之间的“空间”,不包括索引值本身。
- 在这个例子中,间隙有:
(-∞, 20)
,(20, 23)
,(23, 26)
,(26, 30)
,(30, +∞)
5.1.3、Next-Key Locks
- Next-Key Locks是InnoDB的默认锁定模式,它实际上是行锁和间隙锁的组合。
- 对于索引值
20
,Next-Key Lock会锁定(-∞, 20]
的范围,即小于20的所有值的间隙加上索引值20本身(如果它被查询所包括)。 - 对于索引值
23
,Next-Key Lock会锁定(20, 23]
的范围,即20到23之间的间隙加上索引值23本身。 - 对于索引值
26
,Next-Key Lock会锁定(23, 26]
的范围。 - 对于索引值
30
,Next-Key Lock会锁定(26, 30]
的范围,但请注意,由于30是查询范围的结束点,如果查询条件不包括30(即使用< 30
而不是<= 30
),则不会锁定索引值30本身,只会锁定到30之前的间隙。如果查询条件包括30,则索引值30也会被锁定。
[PS:看到这里就会存在疑问了,既然存在行锁和范围间隙锁,那么为什么说在Mysql的默认事务隔离下还是会有可能存在幻读情况呢?这不是矛盾了吗?别急,下面告诉你答案!
]
6、为什么说REPEATABLE READ事务级别可能存在幻读
InnoDB的Next-Key Locking机制在REPEATABLE READ[简称RR]隔离级别下,主要是为了防止幻读,理论上来
说在Mysql默认RR事务隔离级别下是不会产生幻读的,网上说的产生幻读的情况,不能说是RR事务隔离级别造成
的,严格来说是因为Mysql配置或则事务查询策略和事务处理逻辑导致的,在正常情况下,遵循最佳实践和正确的
配置,InnoDB应该能够很好地避免幻读。
由第二节内容咱们了解了什么是幻读,在同一个事务内两次同样的查询,结果不一致的情况称之为“幻读”;
目前通过网上资料的查询和自己以往的经验,Mysql的默认RR事务级别可能存在幻读的原因有以下几种情况:
6.1、锁退化
InnoDB的Next-Key Locking机制在REPEATABLE READ隔离级别下,主要是为了防止幻读,即在事务中两
次执行相同的范围查询时,第二次查询返回了第一次查询时不存在的行。然而,Next-Key Locking在某些情况
下会退化,即它可能不会锁定查询范围内的所有间隙,这可能在特定场景下导致幻读。以下是一些Next-Key
Locking可能退化的情况:
1、唯一索引查询:
如果查询是基于唯一索引(包括主键)的范围查询,InnoDB通常会使用Next-Key Locking来锁定范围内
的行和间隙。但是,如果查询是基于唯一索引的精确匹配,InnoDB只会锁定该行,而不会锁定周围的间隙。
这种理论上不会出现幻读。
2、索引扫描的范围:
当查询的范围非常宽泛,以至于覆盖了大部分或全部的索引时,InnoDB可能不会为每个可能的间隙都加
锁,以避免过多的锁定开销。在这种情况下,Next-Key Locking可能会退化为简单的行锁。【有可能产生幻读。】
3、索引的最左前缀原则:
如果查询没有使用索引的最左前缀,InnoDB可能不会为查询范围内的所有间隙加锁,这可能导致其他事
务能够插入新行而不被锁定。【有可能产生幻读。】
4、索引统计信息:
InnoDB会根据索引统计信息来决定是否需要为查询范围内的间隙加锁。如果统计信息表明范围内的行数
很少,InnoDB可能不会为所有间隙加锁。【有可能产生幻读】
5、系统参数和优化:
InnoDB的某些系统参数,如innodb_adaptive_hash_index和innodb_adaptive_max_index_parts,可以
影响索引的使用和Next-Key Locking的行为。这些参数的设置可能会影响InnoDB是否以及如何使用Next-Key
Locking。【有可能产生幻读】
6、查询优化器的选择:
MySQL的查询优化器可能会选择不同的执行计划,这可能影响InnoDB是否使用Next-Key Locking。例
如,如果优化器选择了全表扫描而不是索引扫描,那么Next-Key Locking将不会被使用。
6.2、事务数据可见性
在REPEATABLE READ隔离级别下,事务确实可以看到自己所做的更改(即,事务内的操作对其自身是可见
的),但对于其他事务所做的已提交更改的可见性则取决于具体的操作和数据库的实现。
对于读取操作(SELECT),RR确保在同一个事务内多次执行相同的查询时,会返回相同的结果集,即使其他
事务已经提交了更改。这通常是通过多版本并发控制(MVCC)来实现的,事务会看到一个在事务开始时创建的数
据库快照的版本。因此,如果其他事务在事务A开始之后但在其查询之前插入了新数据并提交,那么事务A的查询
可能不会立即看到这些新数据,除非它显式地重新查询或以某种方式刷新了它的视图。
然而,**对于更新(UPDATE)和删除(DELETE)操作,情况就不同了。**当事务A尝试更新或删除一条记录
时,它实际上是在与数据库的最新状态进行交互,而不是仅仅查看一个快照。如果其他事务(如事务B)已经插入
了事务A试图更新或删除的记录并提交了这些更改,那么事务A的更新或删除操作将会影响这些已提交的数据。
下面我们举一个例子说明一下:
PS:现在来看RR是在同一个事务里面是读取的刚开始生成的快照信息
具体说明如下:
- 事务A首先执行了一个查询,但没有看到ID为2的记录(假设此时事务B尚未插入该记录或尚未提交)。
- 然后,事务B插入了ID为2的记录并提交。
- 接着,事务A尝试更新ID为2的记录。由于此时ID为2的记录已经存在于数据库中(由事务B插入并提交),事务A能够找到并更新这条记录。
- 最后,当事务A再次执行查询时,它会看到更新后的ID为2的记录,因为这条记录现在存在于数据库的当前状态中,并且对事务A是可见的(尽管它可能不是事务A开始时看到的那个快照版本的一部分)。
因此,虽然RR隔离级别在读取时提供了一致性的视图,但它并不阻止事务看到或修改其他事务已经提交的更改。
这种设计允许事务在保持一定程度隔离的同时,也能够与数据库的当前状态进行交互。
想必现在你已经对幻读时分了解了,不了解的话再看一遍