多版本并发控制MVCC:Snapshot Read vs Current Read
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC Multi-Version Concurrency Control(注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
MVCC 实现原理
为了实现mvcc, innodb对每一行都加上了两个隐含的列,其中一列存储行被更新的”时间”,另外一列存储行被删除的”时间”. 但是innodb存储的并不是绝对的时间,而是与时间对应的数据库系统的版本号,每当一个事务开始的时候,innodb都会给这个事务分配一个递增的版本号,所以版本号也可以被认为是事务号.对于每一个”查询”语句,innodb都会把这个查询语句的版本号同这个查询语句遇到的行的版本号进行对比,然后结合不同的事务隔离等级,来决定是否返回该行.
下面分别以select、delete、 insert、 update语句来说明:
- SELECT
对于select语句,只有同时满足了下面两个条件的行,才能被返回:
•行的被修改版本号小于或者等于该事务号
•行的被删除版本号要么没有被定义,要么大于事务的版本号:行的删除版本号如果没有被定义,说明该行没有被删除过;如果删除版本号大于当前事务的事务号,说明该行是被该事务后面启动的事务删除的,由于是repeatable read隔离等级,后开始的事务对数据的影响不应该被先开始的事务看见,所以该行应该被返回.
- INSERT
对新插入的行,行的更新版本被修改为该事务的事务号
- DELETE
对于删除,innodb直接把该行的被删除版本号设置为当前的事务号,相当于标记为删除,而不是实际删除
- UPDATE
在更新行的时候,innodb会把原来的行复制一份到回滚段中,并把当前的事务号作为该行的更新版本
事务隔离级别Isolation Level
隔离级别:Isolation Level,也是RDBMS的一个关键特性。下面介绍一下MySQL/InnoDB是如何定义这4种隔离级别的。
MySQL/InnoDB定义的4种隔离级别:(mysql的级别不是ios定义的级别,iso定义的RR存在幻读)
-
Read Uncommited
可以读取未提交记录。此隔离级别,不会使用,忽略。
-
Read Committed (RC)
快照读忽略,本文不考虑。
针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。
-
Repeatable Read (RR)
快照读忽略,本文不考虑。
针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。
-
Serializable
从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
在MYSQL的事务引擎中,INNODB是使用范围最广的。它默认的事务隔离级别是REPEATABLE READ(可重复读),在标准的事务隔离级别定义下,REPEATABLE READ是不能防止幻读产生的。INNODB使用了next-key locks实现了防止幻读的发生。
加锁过程分析
-
SQL1:select * from t1 where id = 10;
-
SQL2:delete from t1 where id = 10;
下面对这两个SQL进行分析。SQL1:select操作均不加锁,采用的是快照读。
-
组合一:id列是主键,RC隔离级别
id是主键时,此SQL只需要在id=10这条记录上加X锁即可。
-
组合二:id列是二级唯一索引,RC隔离级别
若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name='d',id=10]的记录。
-
组合三:id列是二级非唯一索引,RC隔离级别
若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。
-
组合四:id列上没有索引,RC隔离级别
若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。
-
组合五:id列是主键,RR隔离级别
与组合一的一致。
-
组合六:id列是二级唯一索引,RR隔离级别
与组合二的一致。
-
组合七:id列是二级非唯一索引,RR隔离级别
Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录,此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。
-
组合八:id列上没有索引,RR隔离级别
在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。
-
组合九:Serializable隔离级别
在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读(SQL1加共享锁)。
网友评论