一.什么是死锁
要想知道MYSQL死锁产生的原因,就要知道什么是死锁?在了解什么是死锁之前,先来看一个概念:线程安全问题
1.线程安全问题
1.1什么是线程安全问题
线程安全问题,指的是在多线程环境当中,线程并发访问某个资源,从而导致的原子性,内存可见性,顺序性问题,以上这三个问题,会导致程序在执行的过程当中,出现了超出预期的结果
1.2 线程安全问题产生的原因是什么
根本原因是在于各个线程是由操作系统随机进行调度,并且各个线程是抢占式执行的.在多线程的环境当中,存在线程共享的数据,并且:
- 当其中多个线程尝试修改共享变量的值,就有可能引发线程安全问题
- 如果单个线程仅仅读但是不修改共享的变量就不会存在线程安全问题
- 如果多个线程串行化,”排好队",挨个修改变量的值就不会引发线程安全问题
下面就上面列举的三种情况作出说明
(1).修改的操作的原子性问题
如果修改操作是原子的,那也不存在线程安全问题,但是大部分的修改都是非原子性的,如何理解修改操作的原子性呢?举个例子说明,见下面代码以及图示:
static class Counter{
int count;
public void add(){
count++;
}
}
如图所示,此时方法add就是尝试对成员变量"count"进行修改,修改为count+1,这个看似“原子”的操作,站在编译器底层,也就是汇编代码的角度,实际上是非原子的,分为以下三个步骤:
- ①把count的值从内存当中读取出来;(load)
- ②执行count++;(add)
- ③把自增操作之后的count返回到内存当中。(save)
假设count的值为0,这三个操作当中,如果有一个线程(Thread1)执行到load指令,然后被操作系统调度离开了CPU,接着另外一个线程(Thread2)被调度到CPU内核上面执行,连续执行load,add,save指令,此时,save的到内存当中的值为1。
这个时候,Thread1右被重新调度到CPU内核上面执行,由于线程调度是有上下文的,因此,Thread1会继续在刚刚load的值的基础上面进行add,save操作。
那么,此时Thread1 save的值仍然为1.与原先期待的两次++操作之后值为2的期待不一样,如果把load,add,save这三个操作,变成原子的,不可以在中间细分开,那么就会达到预计的两次++操作后值为2
总结:
所谓的原子性问题,就是当线程进入一段代码块,还没有执行完毕的时候,允许其他线程进入这一段代码块,执行代码中的操作
(2).内存可见性问题
内存可见性问题就是:一个线程针对变量进行修改,可以被其他的线程及时看到,这里先来了解一下(JMM)模型:
在Java虚拟机当中,线程之间共享的变量存在于主内存(main memory)当中,每一个线程都拥有一个自己的"工作内存",当线程想要读取一个共享的变量的时候,会先把变量从主内存当中加载到自己的工作内存当中,然后先从工作内存当中修改这个变量的值,修改完之后再把修改操作同步到主内存当中,但是,在多线程的环境下面,一旦有线程修改了共享变量的值,由于编译器优化等等的原因,其他线程读取这个被修改的变量时候,不一定继续读取来自于主内存当中的值,而是仅仅继续读取自己的"工作内存“当中的值,这样,也就造成了读取数据的错误,也就引发了线程安全问题
对应到操作系统当中,就是这样的场景:
当一个线程尝试修改内存当中变量的值的时候,是按照:把内存的变量的值读取到寄存器当中,然后首先修改寄存器当中的值,修改完成之后,再修改内存当中的值,但是,在多线程的环境下面,编译器有可能对这一系列指令进行优化,导致其中一个线程修改完内存当中的变量的值之后,其他线程想再次获取这个变量的值的时候,读取到的数据不是来自于内存,而是仅仅读取寄存器当中还没有来得及更新的值,这样,也就引发了线程安全问题
(3).代码顺序性问题
编译器在保持原有代码逻辑仍然不变的情况下,对一系列指令进行了重新排序,如果在单线程的环境下面,是没有任何问题的,但是在多线程的环境下面,仍然执行指令重排序,就有可能被”优化“过之后的代码逻辑发生了改变,从而引发线程安全问题
2.那么怎么保证线程的安全性呢
这里引入synchronized关键字,还是以上面的代码执行案例说明
在代码当中,引入synchronized(锁),让synchronized修饰的代码块当中的所有指令变为原子性,从而实现让Thread1的三个操作"load,add,save"变成原子,就是使用sychronized修饰add方法.让”load,add,save"这三个操作变为不可再分的原子,代码如下:
static class Counter{
int count;
synchronized public void add(){
count++;
}
}
可以理解为互斥使用,也有的地方称为不可中断:
加上锁(synchronized)之后,执行的效果就变成:当两个线程同时调用add方法的时候,其中一个线程,假如是(thread1)会竞争到"锁",另外一个线程(thread2)会进入阻塞状态,进入阻塞队列,也就是Thread类的状态当中的BLOCKED状态,等待竞争到锁的线程执行完add方法之后,此时会自动"解锁",也就是"缩释放”,Thread2会从阻塞队列当中离开,重新回到就绪队列当中,这个就是所谓的互斥使用:两个线程不可以同时拥有一把锁
所谓的原子性,就是:当其中一个线程进入synchronized修饰的代码块当中,要么全部执行完毕,再允许其他的线程进入这一段代码块,要么都不执行
synchronized还保证了内存的可见性:
在JMM当中,对于synchronized的内存可见性做了两条规定:
- 线程进入同步代码块的时候,将清空这个线程工作内存中共享变量的值,从而使得其他线程如果针对这个变量进行读取操作,一定要去主内存当中读取
- 线程离开synchronized修饰的代码块的时候,将会把这个变量修改之后的状态同步到主内存当中,以此来保证共享变量的可见性。
可以理解为:synchronized可以保证内存的刷新
synchronized还保证了有序性:
在synchronized代码块当中,每次只允许一个线程进入,可以理解为:在synchronized代码块内部,同一时刻是单线程执行的,因此,即使编译器针对一部分代码进行了优化,但是仍然不影响代码原有的执行逻辑。如果是多个线程同时进入同步代码块进行修改,此时再发生编译器优化,就有可能导致线程安全问题的发生。
synchronized保证有序性的原理可以理解为:加synchronized后,依然会发生重排序,只不过,有同步代码块,可以保证只有一个线程执行同步代码中的代码,因此保证了有序性。需要注意的是:synchronized虽然保证了代码的有序性,但是,不可以阻止编译器进行指令重排序
3. 思考一个问题:一个线程连续针对同一把锁,连续加锁两次,是否会产生死锁
一个线程连续针对同一把锁,连续加锁两次,是否会产生死锁?这个问题有两种情况:
- 如果加锁后不会阻塞自己,那么这个锁就是一个特殊的锁,叫做可重入锁
- 如果加锁后会阻塞自己,这个锁就不是可重入锁,如果阻塞了自己,那么就相当于产生了"死锁"
案例说明:
class Counter{
public int count;
synchronized public void add(){
synchronized (this) {
count++;
}
}
}
分析上面代码的执行流程:
当多个线程并发调用add()方法的时候,其中一个线程(Thread1)可以获取到锁,其中,该线程针对调用的对象,count1加锁,此时该线程获得了锁,其余线程进入阻塞状态,被加锁的对象就是(count1)
当进入add()方法内部的时候,遇到了synchronized修饰的代码块,此时Thread1继续尝试获取锁,前面也提到,加锁是线程针对对象加锁,可是当调用add()方法的时候,已经针对(this,也就是该方法的调用者count1)加锁了,当再次尝试获取锁的时候,是否会阻塞呢?
回顾一下什么情况下会出现因为加锁而产生的线程阻塞:当多个线程尝试竞争同一个未被加锁的对象的时候,没有竞争到锁的线程会进入阻塞状态,换而言之:如果一个对象被加锁了,那么其他线程想继续获取到这个对象的锁,那么其他线程都无法获取到,都会进入阻塞状态.
那么此时,针对count1,也就是下一个synchronized代码块当中的this,不允许其他线程继续对this加锁,但是,如果Thread1此时也不允许对synchronized代码块中的代码加锁的话,Thread1也会继续进入阻塞状态,这样的话,所有的线程都进入了阻塞状态.
总结一下:如果一个线程针对同一把锁连续两次加锁,在第二次尝试加锁的时候,不会让自身进入阻塞等待的状态,那么称这个锁是可重入锁,根据代码的执行情况,synchronized就是可重入锁
4.死锁产生的条件
上面介绍了线程安全相关的问题,以及synchronized锁,现在回过头来看看死锁产生的相关条件
举个形象点例子:
张三和李四一起下馆子吃饺子,其中张三先拿到了酱油,张三对李四说:你把醋给我,我就把酱油给你。李四一听很不乐意,他反过来说,你如果把酱油先给我,我才会把醋给你。这个时候,两个人就僵持住了,谁也给不出,也就产生了“死锁"
(1).死锁准确的定义
死锁指的是在一组线程当中,由于线程竞争共享的资源,因为互相等待,因而导致了“永久”阻塞的现象,如果没有外力终止程序,将永远无法结束进程
(2).死锁产生的四个必要条件
1)互斥使用
在上面的场景当中,线程张三拿到了锁(酱油);线程李四也拿到了锁(醋),这个时候,如果线程张三尝试获取被李四加锁的对象”醋“,那么张三就会获取锁失败,进而进入阻塞的状态,李四如果想获取张三的锁(酱油)也同理.所以,互斥使用的含义就是:其中一个线程拿到了锁,其他与当前线程竞争锁的线程就必须进入阻塞状态,等待锁的释放。
2)不可抢占
线程1拿到锁之后,线程1一定要主动释放锁,其他线程才可以获取到
3)请求和保持
在上述的场景当中,线程张三获取到锁"酱油"之后,当他想再次获取到李四的醋的时候,不会因为去尝试获取李四的醋而丢失了本来属于自己的锁(酱油),所以,请求和保持的含义就是:线程1获取到对应的一把锁之后,如果想尝试再次获取其他的锁,当前拥有的锁仍然是保持的,不会丢失
4)循环等待
如上面的场景,张三和李四都在等待对方先释放锁,但是都没有自行先释放锁,这个就是循环等待.再举个例子说明:例如有进程A、进程B和进程C三个进程,进程A请求的资源被进程B占用,进程B请求的资源被进程C占用,进程C请求的资源被进程A占用,于是形成了循环等待条件,如下图:
在线程当中,循环等待的场景就是:两个线程都在等待获取到对应的锁之后,都在等待对方释放锁,这就是循环等待
注意:以上四个条件,缺一不可,是出现死锁的充分必要条件,但是,归根结底,还是因为synchronized是必须要等到获取到锁的线程执行完加锁的代码块之后,其他线程才能继续获取到当前的锁,但是,其他的锁不一定跟synchronized一样
5.如何处理死锁死锁
处理死锁有4种方法:分别为预防死锁、避免死锁、检测死锁和解除死锁
预防死锁:
处理死锁最直接的方法就是破坏造成死锁的4个必要条件中的一个或多个,以防止死锁的发生
避免死锁应当从产生死锁的原因出发:
- 1.尽量避免一个线程同时获取多把锁;
- 2.可以考虑一些特定的业务场景,使用lock.tryLock(timeout),限制加锁的时间,来代替synchronized,因为synchronized是一定要等到正在执行同步代码块的线程解锁之后,其他线程才可以获取锁
- 3.针对锁进行"排序",规定所有线程加锁的顺序
假设一个进程当中有n个线程和m把锁,如果想要加锁的话,可以在代码编写的时候对加锁的顺序进行规定,所有的线程都必须按照从1号锁到2号锁......到m号锁的顺序进行加锁,不可以打乱这个加锁的顺序
在上面的案例中,如果分别对酱油和醋,这两把锁进行编号,规定酱油位1,醋位2,张三和李四都必须按照1,2的顺序来拿酱油和醋,就不会发生相互阻塞的情况了
检测死锁:
这种方法允许系统在运行过程中发生死锁,但是能够检测死锁的发生,并采取适当的措施清除死锁
解除死锁:
当检测出死锁后,采用适当的策略和方法将进程从死锁状态解脱出来
在实际工作中,通常采用有序资源分配法和银行家算法这两种方式来避免死锁,大家可自行了解
好了,死锁的相关问题(线程安全,产生条件,避免死锁,处理死锁)就了解完了,归根到底,总结一下:
死锁指两个或多个事务相互等待对方释放锁,从而导致进程无法继续执行的一种情况.当一个事务需要锁定一些资源时,如果这些资源已经被其他事务锁定,则该事务必须等待其他事务释放锁,才能继续执行.如果多个事务同时等待对方释放锁,就会发生死锁
那么,在mysql当中,也会出现死锁,这里的死锁又是什么呢?
二.MYSQL死锁详解
要想了解MYSQL死锁相关知识,就需要知道MYSQL有哪些锁以及锁的级别
1.锁机制
见:关于mysql锁机制详解
几个锁概念:
共享锁(S)
:允许多个事务同时读取同一份数据,但不允许对数据进行修改,当一个事务获得共享锁时,其他事务只能获得共享锁,不能获得排他锁排他锁(X)
:只允许一个事务对数据进行修改,其他事务不能读取或修改该数据,当一个事务获得排他锁时,其他事务不能获得任何类型的锁意向共享锁(IS)
:表示事务想要在某个数据上获得共享锁,但并不是真正的共享锁,只是一个辅助锁,当一个事务获得意向共享锁时,其他事务可以获得共享锁或意向共享锁,但不能获得排他锁意向排他锁(IX)
:表示事务想要在某个数据上获得排他锁,但并不是真正的排他锁,只是一个辅助锁,当一个事务获得意向排他锁时,其他事务只能获得意向共享锁,不能获得任何类型的锁- 记录锁(record lock):这是一个索引记录锁,它是建立在索引记录上的锁(主键和唯一索引都算),很多时候,锁定一条数据,由于无索引,往往会导致整个表被锁住,建立合适的索引可以防止扫描整个表。如:开两个会话,两个事务,并且都不commit,该表有主键,两个会话修改同一条数据,第一个会话update执行后,第二个会话的update是无法执行成功的,会进入等待状态,但是如果update别的数据行就可以成功. 再例如:开两个会话,两个事务,并且都不commit,并且该表无主键无索引,那么第二个会话不管改什么都会进入等待状态。因为无索引的话,整个表的数据都被第一个会话锁定了。
- 间隙锁(gap lock):MySQL默认隔离级别是可重复读,这个隔离级别为了避免幻读现象,引入了这个间隙锁,对索引项之间的间隙上锁
2.锁级别
mysql锁级别:页级、表级、行级
- 表级锁:开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低
- 行级锁:开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高
- 页面锁:开销和加锁时间界于表锁和行锁之间,会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般
从上面可以看出,表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB,而InnoDB存储引擎采用了一种叫作等待图(wait-for graph)的方法来自动检测死锁,如果发现死锁,就会自动回滚一个事务,下面来看看MySQL中的死锁案例
3.案例讲解
案例一
第一步
打开终端A,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为account数据表中id为1的数据添加排他锁,如下所示:
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account where id =1 for update;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 300 |
+----+--------+---------+
1 row in set (0.00 sec)
第二步
打开终端B,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为account数据表中id为2的数据添加排他锁,如下所示:
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account where id =2 for update;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 2 | 李四 | 350 |
+----+--------+---------+
1 row in set (0.00 sec)
第三步
在终端A为account数据表中id为2的数据添加排他锁,如下所示:
mysql> select * from account where id =2 for update;
此时,线程会一直卡住,因为在等待终端B中id为2的数据释放排他锁
第四步
在终端B中为account数据表中id为1的数据添加排他锁,如下所示:
mysql> select * from account where id = 1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
此时发生了死锁,通过如下命令可以查看死锁的日志信息:
show engine innodb status
通过命令行查看LATEST DETECTED DEADLOCK选项相关的信息,可以发现死锁的相关信息,或者通过配置innodb_print_all_deadlocks(MySQL 5.6.2版本开始提供)参数为ON,将死锁相关信息打印到MySQL错误日志中
案例二
假设有下面这一张表:
id(主键索引) | no(非主键索引) | name |
1 | 1001 | 小明 |
2 | 1002 | 小李 |
3 | 1003 | 小华 |
4 | 1004 | 小黄 |
在这一张表当中,id为主键索引,no为二级索引,name这一列没有任何索引的约束,现在这张表当中,有以上的一些数据,现在,有两个事务,一个事务A,另外一个事务B,下面,根据步骤,分析一下两个事务的执行流程:
第一步
在上述两个事物当中,事务A首先开启了,然后执行一条查询的sql语句,也就是select...for update这样的语句,因为记录的最大值为1004,1007不在这一个范围当中,此时,事务A对于表当中no范围为(1004,+∞)的no索引加上了一把锁间隙锁
第二步
事务B开启了,因为no值为1008的记录,不在范围(1004,+∞)的范围之内,因此,事务B也会加一个间隙锁,范围是(1004,+∞);由于间隙锁之间是互容的,因此事务B在执行select语句的时候,不会发生阻塞
第三步
事务A执行了一条插入的索引为1007的数值,但是,由于事务B对于事务A插入的范围加上了间隙锁,因此事务A一定要等待到事务B释放锁,才可以继续执行
第四步
事务B执行了一条插入的索引值为1008的sql语句,但是,由于事务A对于(1004,+∞)的范围加锁了,因此,事务B一定需要等待到事务A释放锁,才可以继续执行
可以看到,此时,两个事务互相阻塞了
那么在上面的insert语句中,是怎么加锁来执行的呢?
Insert语句在正常执行的时候,是不会生成锁结构的,它是靠聚簇索引自带的一个被称为trx_id的字段来作为隐式锁来保护记录的,只有在特殊的情况下面,才会把隐式锁转化为显示锁,也就是真正加锁的过程
举两个例子来说明隐式锁转换为显示锁的场景:
(1).范围(a,b)内加有间隙锁,当有一条记录在范围(a,b)之内插入记录的时候,就会转化为显示锁
(2).如果insert语句插入的记录和已有的记录之间出现了主键,也无法插入
id(主键索引) | no(非主键索引) | name |
1 | 1001 | 小明 |
2 | 1002 | 小李 |
3 | 1003 | 小华 |
4 | 1004 | 小黄 |
此时,这一张表当中,假如有一条语句,执行:
select * from t_order where order_no = 1006 for update;
此时,事务如果插入一条语句,insert....values(1007...),这个时候,由于插入的数据正好在前一个sql语句插入的范围之内,因此会被阻塞
案例三
将投资的钱拆成几份随机分配给借款人,程序思路如下:
投资人投资后,将金额随机分为几份,然后随机从借款人表里面选几个,然后通过一条条select for update 去更新借款人表里面的余额.例如:两个用户同时投资,A用户金额随机分为2份,分给借款人1,2; B用户金额随机分为2份,分给借款人2,1.由于加锁的顺序不一样,就出现了死锁
那么怎么进行改进,避免出现死锁呢,解决:把所有分配到的借款人直接一次锁住,加锁就会从小到大的加锁,然后顺序执行,这就避免了死锁,代码如下:
select * from xxx where id in (xx,xx,xx) for update
在in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁
第一个会话:
注意:关闭掉自动提交set autocommit=0;
mysql> select * from goods where goods_id in (2,3) for update;
+----+--------+------+---------------------+
| good_id | goods_name | price |
+----+--------+------+---------------------+
| 2 | bbbb | 1.00 |
| 3 | vvv | 3.00 |
+----+--------+------+---------------------+
第二个会话:
select * from goods where goods_id in (3,4,5) for update;
锁等待中……
案例四
表锁的死锁情况
用户
A
访问表A
(锁住了表A
),然后又访问表B
;另一个用户B
访问表B
(锁住了表B
),然后企图访问表A
;这时用户A
由于用户B
已经锁住表B
,它必须等待用户B
释放表B
才能继续,同样用户B
要等用户A
释放表A
才能继续,这样就产生了死锁
行锁的死锁情况
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞
解决办法
以上死锁基本是由于程序的
BUG
产生的,在对数据库的多表或单表操作时,尽量按照相同的顺序进行处理且避免同时锁定两个资源.必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源
select for update:
- 如果有唯一索引,命中了唯一记录:行锁,互斥锁;
- 如果有唯一索引,没命中:
gap
锁,另一个事务也可以获得这个gap
锁,但是不能插入数据;(后续有死锁可能)- 如果有普通索引,命中了记录:行锁+
gap
锁;(后续有死锁可能)- 如果有普通索引,没有命中记录:
gap
锁,和情况2相同;(后续有死锁可能)- 如果没有索引,直接锁全表,互斥,直接阻塞别的事务
针对上面出现的情况,再次举个列子说明
CREATE TABLE `test_lock` (
`id` int NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
`city` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_name` (`user_name`),
KEY `idx_city` (`city`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;
insert into test_lock(id,user_name,age,city) values(1,'张三',10,'深圳');
insert into test_lock(id,user_name,age,city) values(5,'李四',32,'成都');
insert into test_lock(id,user_name,age,city) values (9,'王五',55,'广州');
主键索引
BEGIN;
select * from test_lock where id = 1 for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件是主键索引,命中数据库表记录时,一共会加两把锁:一把
IX
意向排他锁(表锁,不影响插入),一把对应主键的X
排他锁(行锁,影响对应主键那一行的插入)
唯一索引
BEGIN;
select * from test_lock where user_name ='张三' for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件是唯一索引,命中数据库表记录时,一共会加三把锁:一把
IX
意向排他锁 (表锁,不影响插入),一把对应主键的X
排他锁(行锁),一把对应唯一索引的X
排他锁 (行锁)
普通索引
BEGIN;
select * from test_lock where city ='成都' for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件是普通索引,命中数据库表记录时,一共会加四把锁:一把
IX
意向排他锁 (表锁,不影响插入),一把对应主键的X
排他锁(行锁),一把对应普通索引的X
排他锁 (行锁),一把对应普通索引的Gap
间隙锁 (锁住一个范围,会影响插入)
非索引
BEGIN;
select * from test_lock where age =10 for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件不是索引,命中或非命中数据库表记录时,都会加一个
IX
锁(表锁,不影响插入),每一行实际记录行的X
锁,还有对应于supremum pseudo-record
的虚拟全表行锁(通俗点讲,其实就是锁表了)
索引未命中
查询条件是中间值
BEGIN;
select * from test_lock where id = 4 for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件是主键,未命中数据库表记录时,查询条件是中间值,会加一个
IX
锁(表锁,不影响插入),一把对应主键索引的X
排他锁 (行锁),一把对应主键索引的Gap
间隙锁 (锁住一个范围,会影响插入)
查询条件非中间值
BEGIN;
select * from test_lock where id = 10 for update;
select * from performance_schema.data_locks;
ROLLBACK;
查询条件是主键,未命中数据库表记录时,查询条件非中间值,会加一个
IX
锁(表锁,不影响插入),每一行实际记录行的X
锁,还有对应于supremum pseudo-record
的虚拟全表行锁(通俗点讲,其实就是锁表了)
4.死锁排查
查看死锁日志:
通过
show engine innodb status
命令查看近期死锁日志信息,主要关注日志中的LATEST DETECTED DEADLOCK
部分,死锁记录只记录最近一个死锁信息,若要将每个死锁信息都保存到错误日志,启用以下参数:
show variables like 'innodb_print_all_deadlocks';
查当前正在运行的InnoDB事务的信息,可以
kill
长期占用锁的事务对应的线程id
select * from information_schema.INNODB_TRX;
5.MySQL中,避免死锁的几种方式
-
尽量让数据表中的数据检索都通过索引来完成,避免无效索引导致行锁升级为表锁
-
合理设计索引,尽量缩小锁的范围
-
尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围
-
尽量控制事务的大小,避免长事务,减少一次事务锁定的资源数量,缩短锁定资源的时间和占用范围,这样可以大大减少死锁产生的概率
-
如果一条SQL语句涉及事务加锁操作,则尽量将其放在整个事务的最后执行
-
尽可能使用低级别的事务隔离机制
-
设置任务超时等待时间:当在一个任务的等待时间超过了这个时间之后,就进行回滚;在 InnoDB 中,参数
innodb_lock_wait_timeout
是用来设置超时时间的,默认值时50秒 -
主动开启死锁检测: 将参数
innodb_deadlock_detect
设置为 on,当innodb检测发现死锁之后,就会进行回滚死锁的事务,但是死锁检测会比较耗资源 -
对于更新频繁的字段,采用唯一索引的设置方案:例如在上面的例子当中,可以把no字段设置成唯一索引
-
保证资源的加锁顺序,避免循环等待的产生
-
使用乐观锁
mvcc
机制,读取数据不上锁,在读情况下共享资源