MVCC 概要
- 事务
- 概念
- 事务的特性:ACID
- 事务的操作
- 隔离性引发的并发问题
- 事务的隔离级别
- LBCC&MVCC
- LBCC
- 记录锁(Record Locks)
- 间隙锁(GAP Locks)
- 临键锁(Next-Key Locks)
- 总结
- 当前读
- 什么是MVCC?
- 什么是当前读和快照读?
- 当前读,快照读和MVCC的关系
- MVCC能解决什么问题,好处是?
- 数据库并发场景有三种,分别为:
- MVCC带来的好处是?
- 小结一下咯
事务
概念
一个事情由n个单元组成,这n个单元在执行过程中,要么同时成功,要么同时失败,这就把n个单元放在了一个事务之中。举个简单的例子:在不考虑试题正确与否的前提下,一张试卷由多个题目构成,当你答完题交给老师的时候是将一整张试卷交给老师,而不是将每道题单独交给老师,在这里试卷就可以理解成一个事务。
事务的特性:ACID
A:原子性(Atomicity
),原子性是指事务是一个不可分割的工作单位,事务中的操作,要么都发生,要么都不发生。
例:假设你在购物车里添加了两件衣服:上衣和裤子,当你把两件衣服作为一个订单提交支付的时候,要么两件衣服一起支付成功,要么都失败,不可能存在上衣付完钱了,裤子还没付完的情况,反之亦然。
C:一致性(Consistency
),在一个事务中,事务前后数据的完整性必须保持一致,期望结果和预期结果一致,列的定义和约束不变。
例:例如用户转账,A通过sql语句扣钱1000,那么最终结果A就是扣了1000,数据上不可能多扣,并且这个列的数据不会因为数据变化导致列的类型或者约束发生变化
I:隔离性(Isolation
),存在于多个事务中,事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
例:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
D:持久性(Durability
),持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
例:我们在操作数据库时,事务提交或者回滚都会直接改变数据库中的值。
事务的操作
在使用事务之前,首先我们要开启事务,我们可以通过start
或者begin
命令开启事务;如果我们想提交事务可以手动执行commit
命令,如果我们想回滚事务,可以执行rollback
命令。
注:在MySQL
中事务的提交是默认开启的,可以执行show variables like 'autocommit'
命令查看,如果是ON
则证明自动提交已经开启,如果为OFF
则需要手动提交。
隔离性引发的并发问题
1)脏读:B事务读取到了A事务尚未提交的数据;
2)不可重复读:B事务读到了A事务已经提交的数据,即B事务在A事务提交之前和提交之后读取到的数据内容
不一致(AB事务操作的是同一条数据);
3)幻读/虚读:B事务读到了A事务已经提交的数据,即A事务执行插入操作,B事务在A事务前后读到的数据数量
不一致。
事务的隔离级别
为了解决以上隔离性引发的并发问题,数据库提供了事物的隔离机制。
- read uncommitted(读未提交): 一个事务还没提交时,它做的变更就能被别的事务看到,读取尚未提交的数据,哪个问题都不能解决;
- read committed(读已提交):一个事务提交之后,它做的变更才会被其他事务看到,读取已经提交的数据,可以解决脏读 ----
oracle
默认的; - repeatable read(可重复读):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,可以解决脏读和不可重复读 —
mysql
默认的; - serializable(串行化):顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。可以解决脏读、不可重复读和虚读—相当于锁表。
虽然serializable
级别可以解决所有的数据库并发问题,但是它会在读取的每一行数据上都加锁,这就可能导致大量的超时和锁竞争问题,从而导致效率下降。所以我们在实际应用中也很少使用serializable
,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
LBCC&MVCC
InnoDB
默认的事务隔离级别是repeatable read
(后文中用简称RR),它为了解决该隔离级别下的幻读的并发问题,提出了LBCC
和MVCC
两种方案。其中LBCC
解决的是当前读情况下的幻读,MVCC
解决的是普通读(快照读)的幻读。
LBCC
LBCC
是Lock-Based Concurrent Control
的简称,意思是基于锁的并发控制。在InnoDB
中按锁的模式来分的话可以分为共享锁(S)、排它锁(X)和意向锁,其中意向锁又分为意向共享锁(IS)和意向排它锁(IX);如果按照锁的算法来分的话又分为记录锁(Record Locks
)、间隙锁(Gap Locks
)和临键锁(Next-key Locks
)。其中临键锁就可以用来解决RR下的幻读问题。那么什么是临键锁呢?继续往下看。
我们将数据库中存储的每一行数据称为记录。则上图中1、5、9、11分别代表id为当前数的记录。对于键值在条件范围内但不存在的记录,叫做间隙(GAP)。则上图中的(-∞,1)、(1,5)…(11,+∞)为数据库中存在的间隙。而(-∞,1]、(1,5]…(11,+∞)我们称之为临键,即左开右闭的集合。
记录锁(Record Locks)
对表中的行记录加锁,叫做记录锁,简称行锁。可以使用sql
语句select ... for update
来开启锁,select
语句必须为精准匹配(=),不能为范围匹配,且匹配列字段必须为唯一索引或者主键列。也可以通过对查询条件为主键索引或唯一索引的数据行进行UPDATE
操作来添加记录锁。
记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
间隙锁(GAP Locks)
对上面说到的间隙加锁即为间隙锁。间隙锁是对范围加锁,但不包括已存在的索引项。可以使用sql
语句select ... for update
来开启锁,select
语句为范围查询,匹配列字段为索引项,且没有数据返回;或者select
语句为等值查询,匹配字段为唯一索引,也没有数据返回。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。以下是加锁之后,插入操作的例子:
select * from user where id > 15 for update;
//插入失败,因为id20大于15,不难理解
insert into user values(20,'20');
//插入失败,原因是间隙锁锁的是记录间隙,而不是sql,上面的图中间隙锁的最后一个数据是11,也就是说`select`语句的锁范围实际上是(11,+∞),而13在这个区间中,所以也失败。
insert into user values(13,'13');
GAP Locks
只存在于RR隔离级别下,它锁住的是间隙内的数据。加完锁之后,间隙中无法插入其他记录,并且锁的是记录间隙,而非sql
语句。间隙锁之间都不存在冲突关系。
打开间隙锁设置: 以通过命令show variables like 'innodb_locks_unsafe_for_binlog';
来查看 innodb_locks_unsafe_for_binlog
是否禁用。innodb_locks_unsafe_for_binlog
默认值为OFF,即启用间隙锁。因为此参数是只读模式,如果想要禁用间隙锁,需要修改 my.cnf
(windows是my.ini
) 重新启动才行。
#在 my.cnf 里面的[mysqld]添加
[mysqld]
innodb_locks_unsafe_for_binlog = 1
临键锁(Next-Key Locks)
当我们对上面的记录和间隙共同加锁时,添加的便是临键锁(左开右闭的集合加锁)。为了防止幻读,临键锁阻止特定条件的新记录的插入,因为插入时要获取插入意向锁,与已持有的临键锁冲突。可以使用sql
语句select ... for update
来开启锁,select
语句为范围查询,匹配列字段为索引项,且有数据返回;或者select
语句为等值查询,匹配列字段为索引项,不管有没有数据返回。
插入意向锁并非意向锁,而是一种特殊的间隙锁。
总结
- 如果查询没有命中索引,则退化为表锁;
- 如果等值查询唯一索引且命中唯一一条记录,则退化为行锁;
- 如果等值查询唯一索引且没有命中记录,则退化为临近结点的间隙锁;
- 如果等值查询非唯一索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定);如果命中记录,则锁定所有命中行的临键锁,并同时锁定最大记录行下一个区间的间隙锁。
- 如果范围查询唯一索引或查询非唯一索引且命中记录,则锁定所有命中行的临键锁 ,并同时锁定最大记录行下一个区间的间隙锁。
- 如果范围查询索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定)。
当前读
当前读(Locking Read
)也称锁定读,读取当前数据的最新版本,而且读取到这个数据之后会对这个数据加锁,防止别的事务更改即通过next-key
锁(行锁+gap锁)来解决当前读的问题。在进行写操作的时候就需要进行“当前读”,读取数据记录的最新版本,包含以下SQL
类型:select ... lock in share mode
、select ... for update
、update
、delete
、insert
。
什么是MVCC?
LBCC
是基于锁的并发控制,因为锁的粒度过大,会导致性能的下降,因此提出了比LBCC
性能更优越的方法MVCC
。MVCC
是Multi-Version Concurrent Control
的简称,意思是基于多版本的并发控制,通过版本号,避免同一数据在不同事务间的竞争,只存在于InnoDB
引擎下。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。MVCC
的实现依赖于:三个隐藏字段、Undo log
和Read View
,其核心思想就是:只能查找事务id小于等于当前事务ID的行;只能查找删除时间大于等于当前事务ID的行,或未删除的行。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
什么是当前读和快照读?
在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?
- 当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。 - 快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
当前读,快照读和MVCC的关系
- 准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
- 而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
- 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志 ,Read View 等去完成的,具体可以看下面的MVCC实现原理
MVCC能解决什么问题,好处是?
数据库并发场景有三种,分别为:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
小结一下咯
总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
- MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突 - MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题