在mysql中,锁机制看起来很复杂,一堆名词:排他锁、共享锁、表锁、间隙锁、意向锁等等,搞的初学者云里雾里。同时锁的相关知识又跟事务隔离级别、索引等概念有千丝万缕的关系,是面试中的常规问题。mysql
上面的脑图是对mysql锁相关知识的一个梳理,但愿可以帮到你们,让你们可以:面试
按锁的应用场景来看,分为读锁和写锁,读锁又可称为S锁和共享锁;写锁又可称为X锁和排他锁。简单来讲,读锁 = S锁 = 共享锁,一样,写锁 = X锁 = 排他锁。
正如他们的取名,只要碰到排他锁,那么就会阻塞,具体阻塞状况以下表:sql
读锁 | 写锁 | |
---|---|---|
读锁 | 否 | 是 |
写锁 | 是 | 是 |
读锁和写锁是互斥的,读写操做是串行数据库
咱们使用Mysql通常是使用InnoDB存储引擎的。InnoDB和MyISAM有两个本质的区别:并发
对于行锁来讲,也分2种类型的锁mvc
其实事务隔离级别就是经过锁机制来实现的,只不过隐藏了加锁的细节,下面来看看二者的关系。post
你们都知道,innodb的事务隔离级别有4种性能
脏读就是一个事务读取到另外一个事务未提交的数据。出现脏读的本质就是由于操做(修改)完该数据就立马释放掉锁,致使读的数据就变成了无用的或者是错误的数据。spa
避免脏读的作法很简单:就是把释放锁的位置调整到事务提交以后,此时在事务提交前,其余进程是没法对该行数据进行读取的。即读写是串行的。
但Read committed会出现不可重复读,即一个事务读取到另一个事务已经提交的数据,也就是说一个事务能够看到其余事务所作的修改。屡次查询数据库的结果都不同。code
和不可重复读相似,但虚读(幻读)会读到其余事务的插入的数据,致使先后读取不一致。能够把不可重复读理解为数据更新,幻读是数据插入。
innodb经过MVCC解决了不可重复读的问题,MVCC的具体原理下面介绍。同时结合间隙锁,避免了幻读。即innodb的Repeatable read其实不会出现幻读的问题,innodb的事务默认级别就是Repeatable read
那么MVCC到底是一种什么机制,可以解决不可重复读的问题?
MVCC即多版本并发控制,能够简单认为 是行级锁的一个升级版。前面提到,只有读-读场景是不阻塞的,其余只有要写(排他锁)场景,都是阻塞的,必定程度上影响了读写效率。基于提高并发性能的考虑,MVCC通常读写是不阻塞的,因此说MVCC不少状况下避免了加锁的操做。
InnoDB中的MVCC,是经过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的建立时间,一个保存行的删除时间。固然存储的并非实际的时间值,而是系统版本号。没开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会做为此事务的版本号,用来和查询到的每行记录的版本号进行比较。
举个select的例子,InnoDB会根据如下两个条件检查每行记录:
简单总结下,多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增加的时间戳,为每一个修改保存一个版本,版本与事务时间戳关联,读操做只读该事务开始前的数据库的快照。 这样在读操做不用阻塞写操做,写操做不用阻塞读操做的同时,避免了脏读和不可重复读。
MVCC虽然解决了不可重复读问题,可是没法解决幻读,须要配合间隙锁。
首先咱们看个例子,初始表以下:
id | x | y | 建立时间 | 删除时间 |
---|---|---|---|---|
1 | 30 | 10 | 1 | undefined |
很简单,一个自增id,一列x,一列y,假设有个限制条件:x+y <= 100。而后两个事务同时并发执行:
T2和T3提交后,x+y=50+60=110 不符合小于100的要求。
Update的本质是 read --> write,MySQL(innodb)为了解决这个问题,强行把 read 分红了 snapshot read(快照读)和 locking read (当前读)。在 UPDATE 或者 SELECT ... FOR UPDATE 的时候,innodb 引擎实际执行的是当前读。
在一个支持MVCC的并发系统中, 咱们须要支持两种读, 一个是快照读, 一个是当前读。
快照读:简单的select操做,属于快照读,不加锁。
当前读:特殊的读操做,插入/更新/删除操做,属于当前读,须要加锁, 读取的是最新数据。
给一个幻读的例子:
update user set col1='new_val' where id=1; 结果: Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0
begin; insert into user values('A'); commit;
update user set col1='new_val' where id=1; 结果: Query OK, 1 rows affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
问题的本质是由于 update语句的查找阶段至关于select ... for update,这会更新事务A的 ReadView,从而能够读到「其余事务已提交的修改」。即出现了幻读
把上面的例子用事务版本号来解释:
id | col1 | 建立时间 | 删除时间 |
---|---|---|---|
1 | A | 2 | undefined |
id | col1 | 建立时间 | 删除时间 |
---|---|---|---|
1 | A | 2 | 1 |
1 | A | 1 | undefined |
InnoDB 经过加间隙锁的方式,解决幻读。innodb对于键值在条件范围内但并不存在的记录(叫作『间隙』)加锁,这种锁机制就是所谓的间隙锁。 相对的,能够把上面不一样的行锁称为记录锁。
间隙锁产生的条件分惟一索引和普通索引:
id
BETWEEN 5 AND 7 FOR UPDATE具体实验例子能够参考 MySQL的锁机制 - 记录锁、间隙锁、临键锁,很是详细。
针对以上幻读的例子,update语句select * from user where id=1 for update
,id是惟一索引,可是因为id=1的记录不存在,因而产生了间隙锁(排他),能够阻塞其余事务的insert操做。
MySQL(innodb)的选择是容许在快照读以后执行当前读,而且更新 snapshot 镜像的版本。严格来讲,这个结果违反了 repeatable read 隔离级别,,可是 who cares 呢,毕竟官方都说了:“This is not a bug but an intended and documented behavior.”
表锁相对来讲就很简单了,表锁顾名思义,就是锁针对的范围是整张表。表锁开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突几率高,并发度最低。如今考虑这样一个情景:
事务A获取了某一行的排他锁,并未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
事务B想要获取users表的表锁:
LOCK TABLES users READ;
由于共享锁与排他锁互斥,因此事务B得确保:
为了检测是否知足第二个条件,事务 B 必须在确保 users表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。扫描全部行这明显是一个效率不好的作法,因而提出了意向锁。
事务在获取行锁(包括读锁和写锁)的同时会获取表的意向锁(包括读锁和写锁)。
意向锁之间是相互兼容的:
意向共享锁 | 意向排他锁 | |
---|---|---|
意向共享锁 | 兼容 | 兼容 |
意向排他锁 | 兼容 | 兼容 |
意向锁和表级锁之间存在互斥状况
意向共享锁 | 意向排他锁 | |
---|---|---|
表级共享锁 | 兼容 | 互斥 |
表级排他锁 | 互斥 | 互斥 |
这里再次强调下:
这里的排他 / 共享锁指的都是表锁!!!意向锁不会与行级的共享 / 排他锁互斥!!!
意向锁不会与行级的共享 / 排他锁互斥!!!
如今再回过头来看刚才的例子:
事务A获取了某一行的排他锁,并未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
此时,事务A也获取了users表的意向排他锁,
事务B想要获取users表的表锁:
LOCK TABLES users READ;
发现此表存在乎向排他锁,因而事务B被阻塞,直到意向排他锁被释放。