大厂面试官必问的Mysql锁机制

前几天有粉丝和我聊到他找工做面试大厂时被问的问题,由于如今疫情期间,找工做也特别难找。他说面试的题目也比较难,都偏向于一两年的工做经验的面试题。程序员

他说在一面的时候被问到Mysql的面试题,索引那块本身都回答比较满意,可是问到Mysql的锁机制就比较懵了。web

由于平时没有关注Mysql的锁机制,当被问到高并发场景下锁机制是怎么保证数据的一致性的和事务隔离性的。面试

他把他面试的过程分享给了我,Mysql高并发锁机制的问题,几乎面大厂都有被问到,Mysql怎么在高并发下控制并发访问的?sql

我细想了一下,Mysql的锁机制确实很是重要,因此在这里作一个全面的总结整理,便于之后的查阅,也分享给各位读者大大们。数据库

Mysql的锁机制仍是有点难理解的,因此这篇文章采用图文结合的方式讲解难点,帮助你们理解,讲解的主要内容以下图的脑图所示,基本涵盖了Mysql锁机制的全部知识点。session

本文脑图

锁种类

Mysql中锁的分类按照不一样类型的划分能够分红不一样的锁,按照锁的粒度划分能够分红:表锁、页锁、行锁;按照使用的方式划分能够分为:共享锁排它锁;按照思想的划分:乐观锁悲观锁并发

下面咱们对着这几种划分的锁进行详细的解说和介绍,在了解设计者设计锁的概念的同时,也能深刻的理解设计者的设计思想。框架

表锁是粒度最大的锁,开销小,加锁快,不会出现死锁,可是因为粒度太大,所以形成锁的冲突概率大,并发性能低。编辑器

Mysql中MyISAM储存引擎就支持表锁,MyISAM的表锁模式有两种:表共享读锁表独占写锁高并发

当一个线程获取到MyISAM表的读锁的时候,会阻塞其余用户对该表的写操做,可是不会阻塞其它用户对该用户的读操做。

相反的,当一个线程获取到MyISAM表的写锁的时候,就会阻塞其它用户的读写操做对其它的线程具备排它性。

页锁的粒度是介于行锁和表锁之间的一种锁,由于页锁是在BDB中支持的一种锁机制,也不多没人说起和使用,因此这里制做概述,不作详解。

行锁是粒度最小的锁机制,行锁的加锁开销性能大,加锁慢,而且会出现死锁,可是行锁的锁冲突的概率低,并发性能高。

行锁是InnoDB默认的支持的锁机制,MyISAM不支持行锁,这个也是InnoDB和MyISAM的区别之一。

行锁在使用的方式上能够划分为:共享读锁(S锁)排它写锁(X锁)

当一个事务对Mysql中的一条数据行加上了S锁,当前事务不能修改该行数据只能执行度操做,其余事务只能对该行数据加S锁不能加X锁。

如果一个事务对一行数据加了X锁,该事物可以对该行数据执行读和写操做,其它事务不能对该行数据加任何的锁,既不能读也不能写。

悲观锁和乐观锁是在不少框架都存在的一种思想,不要狭义地认为它们是某一种框架的锁机制

数据库管理系统中为了控制并发,保证在多个事务执行时的数据一致性以及事务的隔离性,使用悲观锁和乐观锁来解决并发场景下的问题。

Mysql中悲观锁的实现是基于Mysql自身的锁机制实现,而乐观锁须要程序员本身去实现的锁机制,最多见的乐观锁实现就锁机制是使用版本号实现

乐观锁设计思想的在CAS的运用也是比较经典,以前我写过一篇关于CAS的文章,你们感兴趣的能够参考这一篇[]。

从上面的介绍中说了每一种锁的概念,可是很难说哪种锁就是最好的,锁没有最好的,只有哪一种业务场景最适合哪一种锁,具体业务具体分析。

下面咱们就具体基于Mysql的存储引擎详细的分析每一种锁在存储引擎中的运用和实现。

MyISAM

MyISAM中默认支持的表级锁有两种:共享读锁独占写锁。表级锁在MyISAM和InnoDB的存储引擎中都支持,可是InnoDB默认支持的是行锁。

Mysql中平时读写操做都是隐式的进行加锁和解锁操做,Mysql已经自动帮咱们实现加锁和解锁操做了,如果想要测试锁机制,咱们就要显示的本身控制锁机制。

Mysql中能够经过如下sql来显示的在事务中显式的进行加锁和解锁操做:

// 显式的添加表级读锁
LOCK TABLE 表名 READ // 显示的添加表级写锁 LOCK TABLE 表名 WRITE // 显式的解锁(当一个事务commit的时候也会自动解锁) unlock tables; 复制代码

下面咱们就来测试一下MyISAM中的表级锁机制,首先建立一个测试表employee ,这里要指定存储引擎为MyISAM,并插入两条测试数据:

CREATE TABLE IF NOT EXISTS employee (
 id INT PRIMARY KEY auto_increment,  name VARCHAR(40),  money INT )ENGINE MyISAM  INSERT INTO employee(name, money) VALUES('黎杜', 1000); INSERT INTO employee(name, money) VALUES('非科班的科班', 2000); 复制代码

查看一下,表结果以下图所示:

MyISAM表级写锁

(1)与此同时再开启一个session窗口,而后在第一个窗口执行下面的sql,在session1中给表添加写锁:

LOCK TABLE employee WRITE
复制代码

(2)能够在session2中进行查询或者插入、更新该表数据,能够发现都会处于等待状态,也就是session1锁住了整个表,致使session2只能等待:

(3)在session1中进行查询、插入、更新数据,均可以执行成功:

总结: 从上面的测试结果显示当一个线程获取到表级写锁后,只能由该线程对表进行读写操做,别的线程必须等待该线程释放锁之后才能操做

MyISAM表级共享读锁

(1)接下来测试一下表级共享读锁,一样仍是利用上面的测试数据,第一步仍是在session1给表加读锁。

(2)而后在session1中尝试进行插入、更新数据,发现都会报错,只能查询数据。

(3)最后在session2中尝试进行插入、更新数据,程序都会进入等待状态,只能查询数据,直到session1解锁表session2才能插入、更新数据。

总结: 从上面的测试结果显示当一个线程获取到表级读锁后,该线程只能读取数据不能修改数据,其它线程也只能加读锁,不能加写锁

MyISAM表级锁竞争状况

MyISAM存储引擎中,能够经过查询变量来查看并发场景锁的争夺状况,具体执行下面的sql语句:

show status like 'table%';
复制代码

主要是查看table_locks_waitedtable_locks_immediate的值的大小分析锁的竞争状况。

Table_locks_immediate:表示可以当即得到表级锁的锁请求次数;Table_locks_waited表示不能当即获取表级锁而须要等待的锁请求次数分析,值越大竞争就越严重

并发插入

经过上面的操做演示,详细的说明了表级共享锁和表级写锁的特色。可是在平时的执行sql的时候,这些解锁和释放锁都是Mysql底层隐式的执行的

上面的演示只是为了证实显式的执行事务的过程共享锁和表级写锁的加锁和解锁的特色,实际并不会这么作的。

在咱们平时执行select语句的时候就会隐式的加读锁,执行增、删、改的操做时就会隐式的执行加写锁。

MyISAM存储引擎中,虽然读写操做是串行化的,可是它也支持并发插入,这个须要设置内部变量concurrent_insert的值。

它的值有三个值0、一、2。能够经过如下的sql查看concurrent_insert的默认值为AUTO(或者1)

concurrent_insert的值为NEVER (or 0)表示不支持比并发插入;值为AUTO(或者1)表示在MyISAM表中没有被删除的行,运行另外一个线程从表尾插入数据;值为ALWAYS (or 2)表示无论是否有删除的行,都容许在表尾插入数据。

锁调度

MyISAM存储引擎中,假如同时一个读请求,一个写请求过来的话,它会优先处理写请求,由于MyISAM存储引擎中认为写请求比都请求重要。

这样就会致使,假如大量的读写请求过来,就会致使读请求长时间的等待,或者"线程饿死",所以MyISAM不适合运用于大量读写操做的场景,这样会致使长时间读取不到用户数据,用户体验感极差。

固然能够经过设置low-priority-updates参数,设置请求连接的优先级,使得Mysql优先处理读请求。

InnoDB

InnoDB和MyISAM不一样的是,InnoDB支持行锁事务,行级锁的概念前面以及说了,这里就再也不赘述,事务的四大特性的概述以及实现的原理能够参考这一篇[]。

InnoDB中除了有表锁行级锁的概念,还有Gap Lock(间隙锁)、Next-key Lock锁,间隙锁主要用于范围查询的时候,锁住查询的范围,而且间隙锁也是解决幻读的方案

InnoDB中的行级锁是对索引加的锁,在不经过索引查询数据的时候,InnoDB就会使用表锁

可是经过索引查询的时候是否使用索引,还要看Mysql的执行计划,Mysql的优化器会判断是一条sql执行的最佳策略。

如果Mysql以为执行索引查询还不如全表扫描速度快,那么Mysql就会使用全表扫描来查询,这是即便sql语句中使用了索引,最后仍是执行为全表扫描,加的是表锁。

如果对于Mysql的sql执行原理不熟悉的能够参考这一篇文章[]。最后是否执行了索引查询能够经过explain来查看,我相信这个你们都是耳熟能详的命令了。

InnoDB行锁和表锁

InnoDB的行锁也是分为行级共享读锁(S锁)排它写锁(X锁),原理特色和MyISAM的表级锁两种模式是同样的。

若想显式的给表加行级读锁和写锁,能够执行下面的sql语句:

// 给查询sql显示添加读锁
select ... lock in share mode; // 给查询sql显示添加写锁 select ... for update; 复制代码

(1)下面咱们直接进入锁机制的测试阶段,仍是建立一个测试表,并插入两条数据:

// 先把原来的MyISAM表给删除了
DROP TABLE IF EXISTS employee; CREATE TABLE IF NOT EXISTS employee (  id INT PRIMARY KEY auto_increment,  name VARCHAR(40),  money INT )ENGINE INNODB; // 插入测试数据 INSERT INTO employee(name, money) VALUES('黎杜', 1000); INSERT INTO employee(name, money) VALUES('非科班的科班', 2000); 复制代码

(2)建立的表中能够看出对表中的字段只有id添加了主键索引,接着就是在session1窗口执行begin开启事务,并执行下面的sql语句:

// 使用非索引字段查询,并显式的添加写锁
select * from employee where name='黎杜' for update; 复制代码

(3)而后在session2中执行update语句,上面查询的式id=1的数据行,下面update的是id=2的数据行,会发现程序也会进入等待状态:

update employee set name='ldc' where id =2;
复制代码

可见如果使用非索引查询,直接就是使用的表级锁,锁住了整个表。

(4)如果session1使用的是id来查询,以下图所示:

(5)那么session2是能够成功update其它数据行的,可是这里我建议使用数据量大的表进行测试,由于前面我说过了是否执行索引还得看Mysql的执行计划,对于一些小表的操做,可能就直接使用全表扫描

(6)还有一种状况就是:假如咱们给name字段也加上了普通索引,那么经过普通索引来查询数据,而且查询到多行数据,拿它是锁这多行数据仍是锁整个表呢?

下面咱们来测试一下,首先给name字段添加普通索引,以下图所示:

(6)并插入一条新的数据name值与id=2的值相同,并显式的加锁,以下如果:

(7)当update其它数据行name值不是ldc的也会进入等待状态,而且经过explain来查看是否name='ldc'有执行索引,能够看到sql语句是有执行索引条件的。

结论:从上面的测试锁机制的演示能够得出如下几个结论:

  1. 执行非索引条件查询执行的是表锁。
  2. 执行索引查询是不是加行锁,还得看Mysql的执行计划,能够经过explain关键字来查看。
  3. 用普通键索引的查询,遇到索引值相同的,也会对其余的操做数据行的产生影响。

InnoDB间隙锁

当咱们使用范围条件查询而不是等值条件查询的时候,InnoDB就会给符合条件的范围索引加锁,在条件范围内并不存的记录就叫作"间隙(GAP)"

你们大概都知道在事务的四大隔离级别中,不可重复读会产生幻读的现象,只能经过提升隔离级别到串行化来解决幻读现象。

可是Mysql中的不可重复是已经解决了幻读问题,它经过引入间隙锁的实现来解决幻读,经过给符合条件的间隙加锁,防止再次查询的时候出现新数据产生幻读的问题。

例如咱们执行下面的sql语句,就会对id大于100的记录加锁,在id>100的记录中确定是有不存在的间隙:

Select * from  employee where id> 100 for update;
复制代码

(1)接着来测试间隙锁,新增一个字段num,并将num添加为普通索引、修改以前的数据使得num之间的值存在间隙,操做以下sql所示:

alter table employee add num int not null default 0;
update employee set num = 1 where id = 1; update employee set num = 1 where id = 2; update employee set num = 3 where id = 3; insert into employee values(4,'kris',4000,5); 复制代码

(2)接着在session1的窗口开启事务,并执行下面操做:

(3)同时打开窗口session2,并执行新增语句:

insert into employee values(5,'ceshi',5000,2);  // 程序出现等待
insert into employee values(5,'ceshi',5000,4); // 程序出现等待 insert into employee values(5,'ceshi',5000,6); // 新增成功 insert into employee values(6,'ceshi',5000,0); // 新增成功 复制代码

从上面的测试结果显示在区间(1,3]U[3,5)之间加了锁,是不可以新增数据行,这就是新增num=2和num=4失败的缘由,可是在这个区间之外的数据行是没有加锁的,能够新增数据行

根据索引的有序性,而普通索引是能够出现重复值,那么当咱们第一个sesson查询的时候只出现一条数据num=3,为了解决第二次查询的时候出现幻读,也就是出现两条或者更多num=3这样查询条件的数据。

Mysql在知足where条件的状况下,给(1,3]U[3,5)区间加上了锁不容许插入num=3的数据行,这样就解决了幻读。

这里抛出几种状况接着来测试间隙锁。主键索引(惟一索引)是否会加上间隙所呢?范围查询是否会加上间隙锁?使用不存在的检索条件是否会加上间隙锁?

先来讲说:主键索引(惟一索引)是否会加上间隙所呢?

由于主键索引具备惟一性,不容许出现重复,那么当进行等值查询的时候id=3,只能有且只有一条数据,是不可能再出现id=3的第二条数据。

所以它只要锁定这条数据(锁定索引),在下次查询当前读的时候不会被删除、或者更新id=3的数据行,也就保证了数据的一致性,因此主键索引因为他的惟一性的缘由,是不须要加间隙锁的。

再来讲说第二个问题:范围查询是否会加上间隙锁?

直接在session1中执行下面的sql语句,并在session2中在这个num>=3的查询条件内和外新增数据:

select * from employee where num>=3 for update;
insert into employee values(6,'ceshi',5000,2); // 程序出现等待 insert into employee values(7,'ceshi',5000,4); // 程序出现等待 insert into employee values(8,'ceshi',5000,1); // 新增数据成功 复制代码

咱们来分析如下原理:单查询num>=3的时候,在现有的employee表中知足条件的数据行,以下所示:

id num
3 3
4 5
5 6

那么在设计者的角度出发,我为了解决幻读的现象:在num>=3的条件下是必须加上间隙锁的。

而在小于num=3中,下一条数据行就是num=1了,为了防止在(1,3]的范围中加入了num=3的数据行,因此也给这个间隙加上了锁,这就是添加num=2数据行出现等待的缘由。

最后来讲一说:使用不存在的检索条件是否会加上间隙锁?

假如是查询num>=8的数据行呢?由于employee表并不存在中num=8的数据行,num最大num=6,因此为了解决幻读(6,8]与num>=8也会加上锁。

说到这里我相信不少人已经对间隙锁有了清晰和深刻的认识,能够说是精通了,又能够和面试官互扯了。

假如你是第一次接触Mysql的锁机制,第一次确定是懵的,建议多认真的看几遍,跟着案例敲一下本身深入的去体会,慢慢的就懂了。

死锁

死锁在InnoDB中才会出现死锁,MyISAM是不会出现死锁,由于MyISAM支持的是表锁,一次性获取了全部得锁,其它的线程只能排队等候。

而InnoDB默认支持行锁,获取锁是分步的,并非一次性获取全部得锁,所以在锁竞争的时候就会出现死锁的状况。

虽然InnoDB会出现死锁,可是并不影响InnoDB最受欢成为迎的存储引擎,MyISAM能够理解为串行化操做,读写有序,所以支持的并发性能低下。

死锁案例一

举一个例子,如今数据库表employee中六条数据,以下所示:

其中name=ldc的有两条数据,而且name字段为普通索引,分别是id=2和id=3的数据行,如今假设有两个事务分别执行下面的两条sql语句:

// session1执行
update employee set num = 2 where name ='ldc'; // session2执行 select * from employee where id = 2 or id =3; 复制代码

其中session1执行的sql获取的数据行是两条数据,假设先获取到第一个id=2的数据行,而后cpu的时间分配给了另外一个事务,另外一个事务执行查询操做获取了第二行数据也就是id=3的数据行。

当事务2继续执行的时候获取到id=3的数据行,锁定了id=3的数据行,此时cpu又将时间分配给了第一个事务,第一个事务执行准备获取第二行数据的锁,发现已经被其余事务获取了,它就处于等待的状态。

当cpu把时间有分配给了第二个事务,第二个事务准备获取第一行数据的锁发现已经被第一个事务获取了锁,这样就好了死锁,两个事务彼此之间相互等待。

死锁案例二

第二种死锁状况就是当一个事务开始而且update一条id=1的数据行时,成功获取到写锁,此时另外一个事务执行也update另外一条id=2的数据行时,也成功获取到写锁(id为主键)。

此时cpu将时间分配给了事务一,事务一接着也是update id=2的数据行,由于事务二已经获取到id=2数据行的锁,因此事务已处于等待状态。

事务二有获取到了时间,像执行update id=1的数据行,可是此时id=1的锁被事务一获取到了,事务二也处于等待的状态,所以造成了死锁。

session1 session2
begin;update t set name='测试' where id=1; begin
update t set name='测试' where id=2;
update t set name='测试' where id=2;
等待..... update t set name='测试' where id=1;
等待..... 等待......

死锁的解决方案

首先要解决死锁问题,在程序的设计上,当发现程序有高并发的访问某一个表时,尽可能对该表的执行操做串行化,或者锁升级,一次性获取全部的锁资源。

而后也能够设置参数innodb_lock_wait_timeout,超时时间,而且将参数innodb_deadlock_detect 打开,当发现死锁的时候,自动回滚其中的某一个事务。

总结

上面详细的介绍了MyISAM和InnoDB两种存储引擎的锁机制的实现,并进行了测试。

MyISAM的表锁分为两种模式:共享读锁排它写锁。获取的读锁的线程对该数据行只能读,不能修改,其它线程也只能对该数据行加读锁。

获取到写锁的线程对该数据行既能读也能写,对其余线程对该数据行的读写具备排它性。

MyISAM中默认写优先于去操做,所以MyISAM通常不适合运用于大量读写操做的程序中。

InnoDB的行锁虽然会出现死锁的可能,可是InnoDB的支持的并发性能比MyISAM好,行锁的粒度最小,必定的方法和措施能够解决死锁的发生,极大的发挥InnoDB的性能。

InnoDB中引入了间隙锁的概念来决解出现幻读的问题,也引入事务的特性,经过事务的四种隔离级别,来下降锁冲突,提升并发性能。

相关文章
相关标签/搜索