MySQL死锁是什么及怎么掌握

59次阅读
没有评论

共计 5354 个字符,预计需要花费 14 分钟才能阅读完成。

这篇“MySQL 死锁是什么及怎么掌握”文章的知识点大部分人都不太理解,所以丸趣 TV 小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“MySQL 死锁是什么及怎么掌握”文章吧。

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,两个事务都是顺序获取锁,所以也就打破了相互获取锁的条件,最终完美解决死锁问题。

以上就是关于“MySQL 死锁是什么及怎么掌握”这篇文章的内容,相信大家都有了一定的了解,希望丸趣 TV 小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计5354字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)