什麼是死鎖?下面這篇文章帶大家了解MySQL死鎖。聊聊Mysql出現死鎖的必要條件,如何解決死鎖問題?希望對大家有幫助。
死鎖指的是兩個或兩個以上不同的行程或執行緒中,由於存在共同資源的競爭或進程(或執行緒)間的通訊而導致各個執行緒間相互掛起等待,如果沒有外力作用,最終會引發整個系統崩潰。
#資源獨佔條件
##指多個事務在競爭同一個資源時存在互斥性,即在一段時間內某資源只由一個事務佔用,也可叫獨佔資源(如行鎖)。
#指在一個事務a中已經取得鎖定A,但又提出了新的鎖B請求,而該鎖B已被其它事務b佔有,此時該事務a則會阻塞,但又對自己已獲得的鎖A保持不放。
#指示一個交易a中已經獲得鎖定A,在未提交之前,不能被剝奪,只能在使用完後提交交易再自己釋放。
#指在發生死鎖時,必然存在一個相互取得鎖定過程,即持有鎖A的事務a在取得鎖B的同時,持有鎖B的事務b也在取得鎖A,最終導致相互取得而各個事務都阻塞。
#假設存在一個轉帳情景,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; # 查看事务自动提交状态状态
<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>
執行後的結果:
3.4 死鎖轉帳程序
初始化的餘額為:
##假設在高並發情況下有這種場景,A用戶給B用戶轉帳50元的同時,B用戶也給A用戶轉帳30元。那麼我們的java程式操作的過程和時間軸如下:
1、A用戶給B用戶轉帳50元,需在程式中開啟交易1來執行sql,並取得A的餘額同時鎖住A這條數據。3、在事務1中執行剩下的sql# 事务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;登入後複製
# 获取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;
5、 为什么会出现这种情况呢?
主要是因为事务1和事务2存在相互等待获取锁的过程,导致两个事务都挂起阻塞,最终抛出获取锁超时的异常。
3.5 死锁导致的问题
众所周知,数据库的连接资源是很珍贵的,如果一个连接因为事务阻塞长时间不释放,那么后面新的请求要执行的sql也会排队等待,越积越多,最终会拖垮整个应用。一旦你的应用部署在微服务体系中而又没有做熔断处理,由于整个链路被阻断,那么就会引发雪崩效应,导致很严重的生产事故。
要想解决死锁问题,我们可以从死锁的四个必要条件入手。
由于资源独占条件和不剥夺条件是锁本质的功能体现,无法修改,所以咱们从另外两个条件尝试去解决。
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在互联网中的大量使用,所以死锁问题还是经常会被问到,希望兄弟们能掌握这方面的知识,提高自己的竞争力。
【相关推荐:mysql视频教程】
以上是什麼是死鎖?聊聊對MySQL死鎖的理解的詳細內容。更多資訊請關注PHP中文網其他相關文章!