阿里 - 美团 - 字节面试官必问的 Mysql 锁机制,你真的明白吗
MyISAM 存储引擎中,可以通过查询变量来查看并发场景锁的争夺情况,具体执行下面的 sql 语句:
show status like 'table%';
主要是查看table_locks_waited
和table_locks_immediate
的值的大小分析锁的竞争情况。
Table_locks_immediate
:表示能够立即获得表级锁的锁请求次数;Table_locks_waited
表示不能立即获取表级锁而需要等待的锁请求次数分析,「值越大竞争就越严重」。
并发插入
通过上面的操作演示,详细的说明了表级共享锁和表级写锁的特点。但是在平时的执行 sql 的时候,这些**「解锁和释放锁都是 Mysql 底层隐式的执行的」**。
上面的演示只是为了证明显式的执行事务的过程共享锁和表级写锁的加锁和解锁的特点,实际并不会这么做的。
在我们平时执行 select 语句的时候就会隐式的加读锁,执行增、删、改的操作时就会隐式的执行加写锁。
MyISAM 存储引擎中,虽然读写操作是串行化的,但是它也支持并发插入,这个需要设置内部变量concurrent_insert
的值。
它的值有三个值0、1、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;
可见若是**「使用非索引查询,直接就是使用的表级锁」**,锁住了整个表。
![](https://upload-images.jianshu.io/upload_images/24195226-b0bc558a57f
20f53.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
(4)若是 session1 使用的是 id 来查询,如下图所示:
(5)那么 session2 是可以成功 update 其它数据行的,但是这里我建议使用数据量大的表进行测试,因为前面我说过了**「是否执行索引还得看 Mysql 的执行计划,对于一些小表的操作,可能就直接使用全表扫描」**。
(6)还有一种情况就是:假如我们给 name 字段也加上了普通索引,那么通过普通索引来查询数据,并且查询到多行数据,拿它是锁这多行数据还是锁整个表呢?
下面我们来测试一下,首先给**「name 字段添加普通索引」**,如下图所示:
并插入一条新的数据 name 值与 id=2 的值相同,并显式的加锁,如下若是:
(7)当 update 其它数据行 name 值不是 ldc 的也会进入等待状态,并且通过 explain 来查看是否 name='ldc'有执行索引,可以看到 sql 语句是有执行索引条件的。
结论:从上面的测试锁机制的演示可以得出以下几个结论:
执行非索引条件查询执行的是表锁。
执行索引查询是否是加行锁,还得看 Mysql 的执行计划,可以通过 explain 关键字来查看。
用普通键索引的查询,遇到索引值相同的,也会对其他的操作数据行的产生影响。
InnoDB 间隙锁
当我们使用范围条件查询而不是等值条件查询的时候,InnoDB 就会给符合条件的范围索引加锁,在条件范围内并不存的记录就叫做"间隙(GAP)"
大家大概都知道在事务的四大隔离级别中,不可重复读会产生幻读的现象,只能通过提高隔离级别到串行化来解决幻读现象。
但是 Mysql 中的不可重复是已经解决了幻读问题,它通过引入间隙锁的实现来解决幻读,通过给符合条件的间隙加锁,防止再次查询的时候出现新数据产生幻读的问题。
例如我们执行下面的 sql 语句,就会对 id 大于 100 的记录加锁,在 id>100 的记录中肯定是有不存在的间隙:
Select * from employee where id> 100 for update;
(1)接着来测试间隙锁,新增一个字段 num,并将 num 添加为普通索引、修改之前的数据使得 num 之间的值存在间隙,操作如下 sql 所示:
<pre style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 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 表中满足条件的数据行,如下所示:
那么在设计者的角度出发,我为了解决幻读的现象:在 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 的锁被事务一获取到了,事务二也处于等待的状态,因此形成了死锁。
死锁的解决方案
首先要解决死锁问题,在程序的设计上,当发现程序有高并发的访问某一个表时,尽量对该表的执行操作串行化,或者锁升级,一次性获取所有的锁资源。
然后也可以设置参数innodb_lock_wait_timeout
,超时时间,并且将参数innodb_deadlock_detect
打开,当发现死锁的时候,自动回滚其中的某一个事务。
总结
上面详细的介绍了 MyISAM 和 InnoDB 两种存储引擎的锁机制的实现,并进行了测试。
MyISAM 的表锁分为两种模式:「共享读锁」和「排它写锁」。获取的读锁的线程对该数据行只能读,不能修改,其它线程也只能对该数据行加读锁。
获取到写锁的线程对该数据行既能读也能写,对其他线程对该数据行的读写具有排它性。
MyISAM 中默认写优先于去操作,因此 MyISAM 一般不适合运用于大量读写操作的程序中。
InnoDB 的行锁虽然会出现死锁的可能,但是 InnoDB 的支持的并发性能比 MyISAM 好,行锁的粒度最小,一定的方法和措施可以解决死锁的发生,极大的发挥 InnoDB 的性能。
InnoDB 中引入了间隙锁的概念来决解出现幻读的问题,也引入事务的特性,通过事务的四种隔离级别,来降低锁冲突,提高并发性能。
这里是每天都很困的 JAVA 爱好者,如果您跟我一样每天睡不饱却依然热爱 JAVA!
那就关注我吧!
才疏学浅请多指教。
评论