mysql一致性隔离性_MySQL事务隔离级别、数据一致性与加锁处理分析

本文深入探讨了MySQL事务的隔离级别,包括快照读和当前读的区别,以及不同隔离级别(如Read-Committed、Repeatable-Read和Serializable)对数据一致性和并发性能的影响。在 Repeatable-Read 隔离级别下,通过MVCC和间隙锁防止幻读,确保可重复读。此外,文章分析了SQL在不同场景下的加锁行为,揭示了MySQL如何通过锁机制确保事务的一致性。
摘要由CSDN通过智能技术生成

1.引言

众所周知,事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务具有原子性、一致性、隔离性和持久性。但上述描述都是对事务的抽象描述,如何实现事务呢?不同系统可能有不同方案,一般数据库系统基本都是基于锁来实现事务。而锁正是计算机协调多个进城或线程并发访问共享资源的一种机制。在数据库系统中,锁机制能够保证数据并发访问的一致性、有效性。减少锁冲突将有效提升数据库的性能。

2.MySQL事务隔离级别

2.1.快照读和当前读

与基于锁的并发控制(Lock-Based Concurrency Control)相比,多版本并发控制(MVCC,Multi-Version Concurrency Control)好处是:读不加锁,读写不冲突。在OLTP应用中,读写不冲突是非常重要的,它能够加大的提高系统的并发性能。

在多版本并发控制中,读操作分成两类:快照读(snapshot read):快照读,读取的是记录的可见版本,可能是历史版本,不用加锁。

当前读(current read):读取读,读取的是最新版本,当前读返回的记录,都会加锁,保证其他事务不会并发地修改这条记录。

在一个支持MVCC的并发系统,哪些操作是快照读,哪些操作又是当前读呢?

简单的select操作属于快照读,如下所示。

select * from table where xxx;

特殊读、插入、更新、删除操作,属于当前读,需要加锁,如下所示。

select * from table where xxx lock in share mode;

select * from table where xxx for update;

insert into table values(xxx);

update table set xxx where xxx;

delete from table where xxx;

这些都属于当前读,为了保证其他并发事务不能修改当前记录,需要对读取记录加锁,其中,除了lock in share mode是加S锁外,其他的操作,都是加X锁。

这里大家可能会有疑问,为什么将插入、删除、更新操作也归结为当前读,看了下面这张图大家应该很快就能明白。

从图中,可以看到,对于一个update操作:当update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

2.2.事务隔离级别

事务隔离基本从低到高分为:Read-Uncommited、Read-Commited(RC)、Repeated-Read(RR)、SERIALIZABLE 4种隔离级别。越高的隔离级别,能解决的数据一致性问题越多,但同时也会带来性能损耗、降低并发性。

MySQL在RC和RR隔离级别下,是基于MVCC的并发控制,RC针对快照读不加锁;针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),RC下存在幻读现象。RR针对快照读也不加锁;针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁(间隙锁),新的满足查询条件的记录不能够插入 ,不存在幻读现象。

当MySQL在Serializable这种隔离级别下,MySQL将从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL下不建议使用。

一般来说,RC可解决脏读问题,RR解决不可重复度问题(在后文分析中我们会看到InnoDB的RR解决了部分幻读问题),SERIALIZABLE解决幻读问题。在MySQL中隔离级别默认设置为RR。

MySQL(8.0)默认隔离级别是RR,可通过以下命令查看

select @@transaction_isolation;

@@transaction_isolation|

-----------------------|REPEATABLE-READ |

show variables like '%transaction_isolation%';

Variable_name |Value |

---------------------|---------------|transaction_isolation|REPEATABLE-READ|

2.2.为什么是可重复度

RR隔离级别保证可重复读

Binlog是MySQL的逻辑操作日志,广泛应用于复制和恢复。从MySQL 5.1开始,如果打开语句级Binlog,就不支持RC和Read-Committed隔离级别,如果想使用RC,则必须使用Mixed或Row两种格式的Binlog。如下所示。

mysql> set tx_isolation='read-committed';

Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1 values(1,1);

ERROR 1598 (HY000): Binary logging not possible. Message: Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT'

那为什么要去限制隔离基本呢?原因在于Binlog语句的顺序以commit为序,而实际在DB执行上不同隔离级别下执行顺序不一定和Binlog语句顺序一致!

我们做一个测试,如下。

从以上测试可知,RC隔离级别下,会话2的删除操作会影响会话1的结果,由于Binlog语句是以commit为序,语句级Binlog记录上述操作日志将会是这样:

#会话2

set transaction_isolation='read-committed';

delete from t1 where c1 = 2;

commit;

#会话1

set transaction_isolation='read-committed';

Begin;

update t2 set c2 = 3 where c1 in (select c1 from t1);

update t2 set c2 = 4 where c1 in (select c1 from t1);

select * from t2;

+------+------+| c1 | c2 |

+------+------+| 1 | 4 |

| 2 | 2 |

+------+------+2 rows in set (0.00 sec)

commit;

可以看出在RC隔离级别下,语句级Binlog在DR上执行的结果不正确。

出现不一致的根本原因在于:Binlog要求SQL串行化,而RC隔离级别下做不到串行化,做不到可重复读!

我们再次调整隔离级别到RR,再执行一次,如下所示。

在RR隔离级别下,当会话2执行delete语句时会被对话1阻塞,直到会话1提交commit。delete被阻塞的原因在于:会话1语句update t2 set c2 = 3 where c1 in (select c1 from t1)会先在t1的记录上S锁,接着在t2的满足条件的记录上X锁。由于会话1没提交,会话2的delete语句需要等待会话1的S锁释放,于是阻塞。

因此,在RR中,以上测试会话1、会话2的依次执行,与Binlog的顺序一致,从而保证DB/DR一致。

RR隔离级别避免幻读

除了保证可重复读,MySQL在RR级别下还能一定程度上避免幻读(换读是同时存在主线程和干扰线程,干扰线程新增导致主线程更新失败是其中一种典型的换读案例),如下所示。

在上述例子中,会话2的插入操作被阻塞,原因是RR隔离级别下,还会上间隙锁,对于表t1,update t2 set c2=20 where c1 in(select c1 from t1) 以上的锁包括:(-∞, 1), 1, (1, 10), 10, (10, +∞)。由于对t1做全表扫描,因此,所有记录和间隙都要上锁,其中(x,y)表示间隙锁,数字表示记录锁,全部都是S锁。会话2的insert操作插入5,位于间隙(1,10),需要获得这个间隙的X锁,因此两操作互斥,会话2阻塞。

InnoDB通过gap锁来避免幻象,从而实现SQL的可串行化,保证Binlog的一致性。

小结一下:InnoDB在RR隔离级别下,通过S锁(记录锁或gap锁)保证S锁与X锁的互斥,进而保证了SQL的可串行化。

可重复度和可串行化区别

由上述会话1中,连续两次读不到数据,但更新却成功,并且更新后的相同读操作就能读到数据了,这算不算幻读呢?

其实,RR隔离级别的防止幻象主要是针对写操作的,即只保证写操作的可串行化,因为只有写操作影响Binlog;而读操作是通过MVCC来保证一致性读(无幻象)。然而,可串行化隔离级别要求读写可串行化。使用可串行化重做以上测试。

设置为串行化后,会话2的插入操作被阻塞。由于在串行化下,查询操作不再使用MVCC来保证一致读,而是使用S锁来阻塞其他写操作。因此做到读写可串行化,然而换来就是并发性能的大大降低。

MySQL使用可重复读来作为默认隔离级别的主要原因是语句级的Binlog。RR能提供SQL语句的写可串行化,保证了绝大部分情况的DB/DR一致。为了兼顾并发性能,RR隔离级别保证写操作的可串行化,但不保证读写操作的可串行化。

3.SQL执行与加锁处理分析

上一节我们在分析RR隔离级别如何保证可重复度和不产生幻读时分析了几个SQL的上锁过程,本节准备就InnoDB的上锁问题,展开较为深入的分析和讨论,目标是做到针对任何一条SQL语句,都能完整的分析出这条语句会加什么锁,会产生什么使用风险。

来看下面两条简单的SQL。

SQL1:select * from t1 where id = 10;

SQL2:delete from t1 where id = 10;

请问他们加了什么锁?

针对第一个SQL,大家可能会说不加锁,因为MySQL是基于并发版本控制的,因此,读不加锁。

针对第二个SQL,对id = 10的记录加排它锁,走主键索引。

这个回答对吗,它可能在大部分情况下可能对,但不够严谨,因为它缺少一些前提条件:id列是不是主键?

当前系统的隔离级别是什么?

如果id列不是主键,那么id列有没有其他索引?

id列如果有二级索引,那么这个索引是不是唯一索引?

两个SQL的执行计划是什么?全表扫描还是索引扫描?

了解了这些前提,才好去分析SQL的加锁情况。下面我们分析各种可能的情况,我们假设这两个SQL都会走索引扫描。

3.1.组合一:RC隔离级别 + id主键

这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只需要将主键上,id = 10的记录加上X锁即可。如下图所示:

结论:id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

3.2.组合二:RC隔离级别 + id唯一索引

这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 需要加什么锁呢?见下图:

此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。由于id是unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。为什么聚簇索引上的记录也要加锁?试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。

结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name='d',id=10]的记录。

3.3.组合三:RC隔离级别 + id非唯一索引

相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?同样见下图:

根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只有一个满足等值查询的记录,而组合三会将所有满足查询条件的记录都加锁。

结论:若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。

3.4.组合四:RC隔离级别 + id无索引

相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引,where id = 10;这个过滤条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有很多:有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际情况呢?请看下图:

由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加行锁。

有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

3.5.组合五:RR隔离级别 + id主键

上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在Repeatable Read隔离级别下的加锁行为。

组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed]一致。

3.6.组合六:RR隔离级别 + id唯一索引

与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed]一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。

3.7.组合七:RR隔离级别 + id非唯一索引

还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一致。那么RR隔离级别下,如何防止幻读呢?问题的答案,就在组合七中揭晓。

组合七,Repeatable Read隔离级别,id上有一个非唯一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这幅图:

此图,相对于组合三:[id列上非唯一锁,Read Committed]看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间的位置,GAP锁有何用?

其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。所谓幻读,就是同一个事务,连续做两次当前读 (例如:select * from t1 where id = 10 for update;),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。

如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。

如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。

如果不加gap锁:记录[6,c]之前,不会插入id=10的记录;

[6,c]与[10,b]间可以插入[10, aa];

[10,b]与[10,d]间,可以插入新的[10,bb],[10,c]等;

[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;

而[11,f]之后也不会插入满足条件的记录。

因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。

Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。

有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是RR隔离级别,却不需要加GAP锁呢?

首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了GAP锁的使用。其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能够省略?此问题留给大家思考。

结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

3.8.组合八:RR隔离级别 + id无索引

组合八,Repeatable Read隔离级别下的最后一种情况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图所示:

如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP锁。试想,如果表上有1000万条记录呢?在这种情况下,这个表上,除了不加锁的快照读,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。

当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。

3.9 组合九:Serializable

针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来说,Serializable隔离级别与Repeatable Read隔离级别完全一致,因此不做介绍。

Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

总结

本文首先介绍MVCC下的快照读和当前读,然后分析事务的隔离级别与数据一致性,给出MySQL如何基于锁保证数据的一致性,文章最后一部分分析了SQL执行中在不同隔离级别不同索引下是如何上锁的。

The end.

转载请注明来源,否则严禁转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值