共计 8189 个字符,预计需要花费 21 分钟才能阅读完成。
今天丸趣 TV 小编给大家分享一下 mysql 出现死锁的必要条件是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
在 mysql 中,死锁指的是在两个或两个以上不同的进程或线程中,因争夺资源而造成的一种互相等待的现象;由于存在共同资源的竞争或进程(或线程)间的通讯而导致各个线程间相互挂起等待,如果没有外力作用,最终会引发整个系统崩溃。mysql 出现死锁的必要条件:1、资源独占条件;2、请求和保持条件;3、不剥夺条件;4、相互获取锁条件。
1、什么是死锁?
死锁指的是在两个或两个以上不同的进程或线程中,因争夺资源而造成的一种互相等待的现象;由于存在共同资源的竞争或进程(或线程)间的通讯而导致各个线程间相互挂起等待,如果没有外力作用,最终会引发整个系统崩溃。
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进程称为死锁进程。
2、Mysql 出现死锁的必要条件
资源独占条件
指多个事务在竞争同一个资源时存在互斥性,即在一段时间内某资源只由一个事务占用,也可叫独占资源(如行锁)。
请求和保持条件
指在一个事务 a 中已经获得锁 A,但又提出了新的锁 B 请求,而该锁 B 已被其它事务 b 占有,此时该事务 a 则会阻塞,但又对自己已获得的锁 A 保持不放。
不剥夺条件
指一个事务 a 中已经获得锁 A,在未提交之前,不能被剥夺,只能在使用完后提交事务再自己释放。
相互获取锁条件
指在发生死锁时,必然存在一个相互获取锁过程,即持有锁 A 的事务 a 在获取锁 B 的同时,持有锁 B 的事务 b 也在获取锁 A,最终导致相互获取而各个事务都阻塞。
3、Mysql 经典死锁案例
假设存在一个转账情景,A 账户给 B 账户转账 50 元的同时,B 账户也给 A 账户转账 30 元,那么在这过程中是否会存在死锁情况呢?
3.1 建表语句
CREATE TABLE `account` ( `id` int(11) NOT NULL COMMENT 主键 ,
`user_id` varchar(56) NOT NULL COMMENT 用户 id ,
`balance` float(10,2) DEFAULT NULL COMMENT 余额 ,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT= 账户余额表
3.2 初始化相关数据
INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (1, A , 80.00);
INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (2, B , 60.00);
3.3 正常转账过程
在说死锁问题之前,咱们先来看看正常的转账过程。
正常情况下,A 用户给 B 用户转账 50 元,可在一个事务内完成,需要先获取 A 用户的余额和 B 用户的余额,因为之后需要修改这两条数据,所以需要通过写锁(for UPDATE)锁住他们,防止其他事务更改导致我们的更改丢失而引起脏数据。
相关 sql 如下:
开启事务之前需要先把 mysql 的自动提交关闭
set autocommit=0;
# 查看事务自动提交状态状态
show VARIABLES like autocommit ![在这里插入图片描述](https://img-blog.csdnimg.cn/a486a4ed5c9d4240bd115ac7b3ce5a39.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_
Q1NETiBA6ZqQIOmjjg==,size_20,color_FFFFFF,t_70,g_se,x_16)
# 转账 sql
START TRANSACTION;
# 获取 A 的余额并存入 A_balance 变量:80
SELECT user_id,@A_balance:=balance from account where user_id = A for UPDATE;
# 获取 B 的余额并存入 B_balance 变量:60
SELECT user_id,@B_balance:=balance from account where user_id = B for UPDATE;
# 修改 A 的余额
UPDATE account set balance = @A_balance - 50 where user_id = A
# 修改 B 的余额
UPDATE account set balance = @B_balance + 50 where user_id = B
COMMIT;
执行后的结果:
可以看到数据更新都是正常的情况
3.4 死锁转账过程
初始化的余额为:
假设在高并发情况下存在这种场景,A 用户给 B 用户转账 50 元的同时,B 用户也给 A 用户转账 30 元。
那么我们的 java 程序操作的过程和时间线如下:
1.A 用户给 B 用户转账 50 元,需在程序中开启事务 1 来执行 sql,并获取 A 的余额同时锁住 A 这条数据。
# 事务 1
set autocommit=0;
START TRANSACTION;
# 获取 A 的余额并存入 A_balance 变量:80
SELECT user_id,@A_balance:=balance from account where user_id = A for UPDATE;
2.B 用户给 A 用户转账 30 元,需在程序中开启事务 2 来执行 sql,并获取 B 的余额同时锁住 B 这条数据。
# 事务 2
set autocommit=0;
START TRANSACTION;
# 获取 A 的余额并存入 A_balance 变量:60
SELECT user_id,@A_balance:=balance from account where user_id = B for UPDATE;
3. 在事务 1 中执行剩下的 sql
# 获取 B 的余额并存入 B_balance 变量:60
SELECT user_id,@B_balance:=balance from account where user_id = B for UPDATE;
# 修改 A 的余额
UPDATE account set balance = @A_balance - 50 where user_id = A
# 修改 B 的余额
UPDATE account set balance = @B_balance + 50 where user_id = B
COMMIT;
可以看到,在事务 1 中获取 B 数据的写锁时出现了超时情况。为什么会这样呢?主要是因为我们在步骤 2 的时候已经在事务 2 中获取到 B 数据的写锁了,那么在事务 2 提交或回滚前事务 1 永远都拿不到 B 数据的写锁。
4. 在事务 2 中执行剩下的 sql
# 获取 A 的余额并存入 B_balance 变量:60
SELECT user_id,@B_balance:=balance from account where user_id = A for UPDATE;
# 修改 B 的余额
UPDATE account set balance = @A_balance - 30 where user_id = B
# 修改 A 的余额
UPDATE account set balance = @B_balance + 30 where user_id = A
COMMIT;
同理可得,在事务 2 中获取 A 数据的写锁时也出现了超时情况。因为步骤 1 的时候已经在事务 1 中获取到 A 数据的写锁了,那么在事务 1 提交或回滚前事务 2 永远都拿不到 A 数据的写锁。
5. 为什么会出现这种情况呢?
主要是因为事务 1 和事务 2 存在相互等待获取锁的过程,导致两个事务都挂起阻塞,最终抛出获取锁超时的异常。
3.5 死锁导致的问题
众所周知,数据库的连接资源是很珍贵的,如果一个连接因为事务阻塞长时间不释放,那么后面新的请求要执行的 sql 也会排队等待,越积越多,最终会拖垮整个应用。一旦你的应用部署在微服务体系中而又没有做熔断处理,由于整个链路被阻断,那么就会引发雪崩效应,导致很严重的生产事故。
4、如何解决死锁问题?
要想解决死锁问题,我们可以从死锁的四个必要条件入手。
由于资源独占条件和不剥夺条件是锁本质的功能体现,无法修改,所以咱们从另外两个条件尝试去解决。
4.1 打破请求和保持条件
根据上面定义可知,出现这个情况是因为事务 1 和事务 2 同时去竞争锁 A 和锁 B,那么我们是否可以保证锁 A 和锁 B 一次只能被一个事务竞争和持有呢?
答案是肯定可以的。下面咱们通过伪代码来看看:
/**
* 事务 1 入参 (A, B)
* 事务 2 入参 (B, A)
public void transferAccounts(String userFrom, String userTo) {
// 获取分布式锁
Lock lock = Redisson.getLock();
// 开启事务
JDBC.excute( START TRANSACTION;
// 执行转账 sql
JDBC.excute( # 获取 A 的余额并存入 A_balance 变量:80\n +
SELECT user_id,@A_balance:=balance from account where user_id = + userFrom + for UPDATE;\n +
# 获取 B 的余额并存入 B_balance 变量:60\n +
SELECT user_id,@B_balance:=balance from account where user_id = + userTo + for UPDATE;\n +
\n +
# 修改 A 的余额 \n +
UPDATE account set balance = @A_balance - 50 where user_id = + userFrom + \n +
# 修改 B 的余额 \n +
UPDATE account set balance = @B_balance + 50 where user_id = + userTo + \n
// 提交事务
JDBC.excute( COMMIT;
// 释放锁
lock.unLock();}
上面的伪代码显而易见可以解决死锁问题,因为所有的事务都是通过分布式锁来串行执行的。
那么这样就真的万事大吉了吗?
在小流量情况下看起来是没问题的,但是在高并发场景下这里将成为整个服务的性能瓶颈,因为即使你部署了再多的机器,但由于分布式锁的原因,你的业务也只能串行进行,服务性能并不因为集群部署而提高并发量,完全无法满足分布式业务下快、准、稳的要求,所以咱们不妨换种方式来看看怎么解决死锁问题。
4.2 打破相互获取锁条件(推荐)
要打破这个条件其实也很简单,那就是事务再获取锁的过程中保证顺序获取即可,也就是锁 A 始终在锁 B 之前获取。
我们来看看之前的伪代码怎么优化?
/**
* 事务 1 入参 (A, B)
* 事务 2 入参 (B, A)
public void transferAccounts(String userFrom, String userTo) {
// 对用户 A 和 B 进行排序,让 userFrom 始终为用户 A,userTo 始终为用户 B
if (userFrom.hashCode() userTo.hashCode()) {
String tmp = userFrom;
userFrom = userTo;
userTo = tmp;
}
// 开启事务
JDBC.excute( START TRANSACTION;
// 执行转账 sql
JDBC.excute( # 获取 A 的余额并存入 A_balance 变量:80\n +
SELECT user_id,@A_balance:=balance from account where user_id = + userFrom + for UPDATE;\n +
# 获取 B 的余额并存入 B_balance 变量:60\n +
SELECT user_id,@B_balance:=balance from account where user_id = + userTo + for UPDATE;\n +
\n +
# 修改 A 的余额 \n +
UPDATE account set balance = @A_balance - 50 where user_id = + userFrom + \n +
# 修改 B 的余额 \n +
UPDATE account set balance = @B_balance + 50 where user_id = + userTo + \n
// 提交事务
JDBC.excute( COMMIT;
}
假设事务 1 的入参为 (A, B),事务 2 入参为 (B, A),由于我们对两个用户参数进行了排序,所以在事务 1 中需要先获取锁 A 在获取锁 B,事务 2 也是一样要先获取锁 A 在获取锁 B,两个事务都是顺序获取锁,所以也就打破了相互获取锁的条件,最终完美解决死锁问题。
5、如何预防死锁
阻止死锁的途径就是避免满足死锁条件的情况发生,为此我们在开发的过程中需要遵循如下原则:
1. 尽量避免并发的执行涉及到修改数据的语句。
2. 要求每一个事务一次就将所有要使用到的数据全部加锁,否则就不允许执行。
3. 预先规定一个加锁顺序,所有的事务都必须按照这个顺序对数据执行封锁。如不同的过程在事务内部对对象的更新执行顺序应尽量保证一致。
4. 每个事务的执行时间不可太长,对程序段的事务可考虑将其分割为几个事务。在事务中不要求输入,应该在事务之前得到输入,然后快速执行事务。
5. 使用尽可能低的隔离级别。
6. 数据存储空间离散法。该方法是指采用各种手段,将逻辑上在一个表中的数据分散的若干离散的空间上去,以便改善对表的访问性能。主要通过将大表按行或者列分解为若干小表,或者按照不同的用户群两种方法实现。
7. 编写应用程序,让进程持有锁的时间尽可能短,这样其它进程就不必花太长的时间等待锁被释放。
死锁的概念:
如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么改组进程是死锁的。
死锁的常见表现:
死锁不仅会发生多个进程中,也会发生在一个进程中。
(1)多进程死锁:有进程 A,进程 B,进程 A 拥有资源 1,需要请求正在被进程 B 占有的资源 2。而进程 B 拥有资源 2,请求正在被进程 A 战友的资源 1。两个进程都在等待对方释放资源后请求该资源,而相互僵持,陷入死锁。
(2)单进程死锁:进程 A 拥有资源 1,而它又在请求资源 1,而它所请求的资源 1 必须等待该资源使用完毕得到释放后才可被请求。这样,就陷入了自己的死锁。
产生死锁的原因:
(1)进程推进顺序不当造成死锁。
(2)竞争不可抢占性资源引起死锁。
(3)竞争可消耗性资源引起死锁。
死锁的四个必要条件(四个条件四者不可缺一):
(1)互斥条件。某段时间内,一个资源一次只能被一个进程访问。
(2)请求和保持条件。进程 A 已经拥有至少一个资源,此时又去申请其他资源,而该资源又正在被进程使用,此时请求进程阻塞,但对自己已经获得的资源保持不放。
(3)不可抢占资源。进程已获得的资源在未使用完不能被抢占,只能在自己使用完时由自己释放。
(4)循环等待序列。存在一个循环等待序列 P0P1P2……Pn,P0 请求正在被进程 P1 占有的资源,P1 请求正在被 P2 占有的资源……Pn 正在请求被进程 P0 占有的资源。
解除死锁的两种方法:
(1)终止(或撤销)进程。终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解除出来。
(2)抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以打破死锁状态。
6、死锁场景
本文死锁场景皆为工作中遇到(或同事遇到)并解决的死锁场景,写这篇文章的目的是整理和分享,欢迎指正和补充,本文死锁场景包括:
行锁导致死锁
gap lock/next keys lock 导致死锁
index merge 导致死锁
唯一索引冲突导致死锁
注:以下场景隔离级别均为默认的 Repeatable Read;
1)行锁导致死锁
死锁原因详解:
1. 两个事务执行过程时间上有交集,并且过程发生在两者提交之前
2. 事务 1 更新 uid= 1 的记录,事务 2 更新 uid= 2 的记录,在 RR 级别,由于 uid 是唯一索引,因此两个事务将分别持有 uid= 1 和 2 所在行的独占锁
3. 事务 1 执行到第二条更新语句时,发现 uid= 2 的行被锁住,进入阻塞等待锁释放;
4. 事务 2 执行到第二条语句时发现 uid= 1 的行被锁,同样进入阻塞
5. 两个事务互相等待,死锁产生。
相应业务案例和解决方案:
该场景常见于事务中存在 for 循环更新某条记录的情况,死锁日志显示 lock_mode X locks rec but not gap waiting(即行锁而非间隙锁),解决方案:
1. 避免循环更新,优化为一条 where 锁定要更新的记录批量更新
2. 如果非要循环更新,尝试取消事务(能接受的话),即每一条更新为一个独立的事务
2)gap lock/next keys lock 导致死锁
死锁原因分析:
1. 事务 1 执行 delete age = 27,务 2 执行 delete age = 31,在 RR 级别,操作条件不是唯一索引时,行锁会升级为 next keys
lock(可以理解为间隙锁),因此事务 1 锁住了 25 到 27 和 27 到 29 的区间,事务 2 锁住了 29 到 31 的区间
2. 事务 1 执行 insert age = 30,等待事务 2 释放锁
3. 事务 2 执行 insert age = 28,等待事务 1 释放锁
4. 死锁产生,死锁日志显示 lock_mode X locks gap before rec insert intention waiting
解决方案:
1. 降低事务隔离级别到 Read Committed,该隔离级别下间隙锁降级为行锁,可以减少死锁发生的概率
2. 避免这种场景 - –
3)index merge 导致死锁
t_user 结构改造为:
死锁分析:
1. 在符合场景前提的情况下(即表数据量较大,index_merge 未关闭),通过 explain 分析 update t_user where zone_id = 1 and uid = 1 可以发现 type 是 index_merge,即会用到 zone_id 和 uid 两个索引
2. 上锁的过程为:
事务 1:
① 锁住 zone_id= 1 对应的间隙锁: zoneId in (1,2)
② 锁住索引 zone_id= 1 对应的主键索引行锁 id = [1,2]
③ 锁住 uid= 1 对应的间隙锁: uid in (1, 2)
④ 锁住 uid= 1 对应的主键索引行锁: id = [1, 3]
事务 2:
① 锁住 zone_id= 2 对应的间隙锁: zoneId in (1,2)
② 锁住索引 zone_id= 2 对应的主键索引行锁 id = [3,4]
③ 锁住 uid= 2 对应的间隙锁: uid in (1,2)
④ 锁住 uid= 2 对应的主键索引行锁: id = [2, 4]
1、如果两个事务上锁的顺序相反,则有一定的概率出现死锁。另外,index_merge 的形式锁住了很多不符合条件的行,浪费了资源。一般死锁日志打印的信息为:
lock_mode
X locks rec but not gap waiting Record lock
解决方案:创建联合索引,使执行计划只会用到一个索引。
注:
update table set name = “wea” where col_1 = 1 or col_2 = 2 ;
col_1 和 col_2 为联合索引,遵循最左原则 col_1 会走索引,但 col_2 会对整个索引进行扫描,此时会对整个索引加锁。
以上就是“mysql 出现死锁的必要条件是什么”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,丸趣 TV 小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注丸趣 TV 行业资讯频道。