什麼是死鎖?聊聊對MySQL死鎖的理解

青灯夜游
發布: 2022-08-24 10:14:30
轉載
2220 人瀏覽過

什麼是死鎖?下面這篇文章帶大家了解MySQL死鎖。聊聊Mysql出現死鎖的必要條件,如何解決死鎖問題?希望對大家有幫助。

什麼是死鎖?聊聊對MySQL死鎖的理解

1、什麼是死鎖?

死鎖指的是兩個或兩個以上不同的行程或執行緒中,由於存在共同資源的競爭或進程(或執行緒)間的通訊而導致各個執行緒間相互掛起等待,如果沒有外力作用,最終會引發整個系統崩潰。

2、Mysql出現死鎖的必要條件

  • #資源獨佔條件

##指多個事務在競爭同一個資源時存在互斥性,即在一段時間內某資源只由一個事務佔用,也可叫獨佔資源(如行鎖)。

  • 請求與保留條件

#指在一個事務a中已經取得鎖定A,但又提出了新的鎖B請求,而該鎖B已被其它事務b佔有,此時該事務a則會阻塞,但又對自己已獲得的鎖A保持不放。

  • 不剝奪條件

#指示一個交易a中已經獲得鎖定A,在未提交之前,不能被剝奪,只能在使用完後提交交易再自己釋放。

  1. 相互取得鎖定條件

#指在發生死鎖時,必然存在一個相互取得鎖定過程,即持有鎖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_d3F5#LXplbmhlaQ,shadow_50,text_##IOIO106FFIO205,#FIO20,21,#FIO size_20,color_FFFFFF,t_70,g_se,x_16)

<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"># 转账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;</pre><div class="contentsignin">登入後複製</div></div>執行後的結果:

可以看到資料更新都是正常的情況

什麼是死鎖?聊聊對MySQL死鎖的理解3.4 死鎖轉帳程序

初始化的餘額為:

##假設在高並發情況下有這種場景,A用戶給B用戶轉帳50元的同時,B用戶也給A用戶轉帳30元。

那麼我們的java程式操作的過程和時間軸如下​​:什麼是死鎖?聊聊對MySQL死鎖的理解

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、总结

因为mysql在互联网中的大量使用,所以死锁问题还是经常会被问到,希望兄弟们能掌握这方面的知识,提高自己的竞争力。

【相关推荐:mysql视频教程

以上是什麼是死鎖?聊聊對MySQL死鎖的理解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板