当前位置: 首页 > 后端技术 > Java

阿里我来说说你对Mysql死锁的理解

时间:2023-04-01 19:42:54 Java

又到了金三银四的时候了。每个人都无法忍受心中的不安。在这里跟大家分享一下我在之前面试中遇到的一个知识点。(死锁问题),如有不足,欢迎指出。1、什么是死锁?死锁是指在两个或多个不同的进程或线程中,由于对公共资源的竞争或进程(或线程)之间的通信,导致各个线程挂起并互相等待。如果没有外力,最终会导致整个系统崩溃。2、Mysql死锁的必要条件资源独占条件是指多个事务在竞争同一个资源时具有互斥性,即一个资源在一段时间内只被一个事务占用,也可以称为一个独占资源(如行锁)。request-hold条件是指在一个事务a中已经获得了锁A,但是又请求了一个新的锁B,锁B已经被其他事务b占用了。获取的锁A保持保留状态。非剥夺条件是指在事务a中已经获得了锁A,在提交前不能被剥夺。使用后提交交易后才能释放。相互锁获取条件是指发生死锁时,必须有一个相互获取锁的过程,即持有锁A的事务a获取锁B,同时持有锁B的事务b也在获取锁A,最终导致相互Getwhileeach交易块。3.Mysql经典死锁案例假设有一个转移场景。当A账户向B账户转50元时,B账户也向A账户转30元,这个过程中会不会出现死锁的情况?3.1建表语句CREATETABLE`account`(`id`int(11)NOTNULLCOMMENT'primarykey',`user_id`varchar(56)NOTNULLCOMMENT'userid',`balance`float(10,2)DEFAULTNULLCOMMENT'balance',PRIMARYKEY(`id`),UNIQUEKEY`idx_user_id`(`user_id`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='accountbalancetable';3.2初始化相关数据INSERTINTO`test`.`account`(`id`,`user_id`,`balance`)VALUES(1,'A',80.00);INSERTINTO`test`.`account`(`id`,`user_id`,`balance`)VALUES(2,'B',60.00);3.3正常的传输过程在说死锁问题之前,我们先来看一下正常的传输过程。正常情况下,用户A向用户B转账50元,一笔交易即可完成。你需要先获取用户A和用户B的余额。因为这两个数据后面需要修改,所以需要通过写锁(针对UPDATE)给它们加锁,防止其他事务性的改变导致我们的改变丢失,造成脏数据。相关sql如下:==开始事务之前,需要关闭mysql的autocommit==setautocommit=0;#查看事务的自动提交状态showVARIABLESlike'autocommit';![Insertpicturedescriptionhere](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_sql16余额存入A_balance变量:80SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='A'forUPDATE;#GetB'sbalance并将其存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='B'forUPDATE;#ModifyA'sbalanceUPDATEaccountsetbalance=@A_balance-50whereuser_id='A';#ModifyB'sbalanceUPDATEaccountsetbalance=@B_balance+50whereuser_id='B';COMMIT;执行后结果:可以看出数据更新正常3.4死锁转账过程初始余额为:假设有这样一个高并发下的场景,当用户A给用户B转了50元,用户B也给用户A转了30元。那么我们java程序运行的流程和时间线如下:用户A给用户B转了50元,需要程序中开启事务1执行sql,获取A的余额并锁定A的数据。#Transaction1setautocommit=0;STARTTRANSACTION;#获取A的余额存入A_balance变量:80SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='A'forUPDATE;程序中启动事务2执行sql,获取B的余额,同时锁定B的数据。#Transaction2setautocommit=0;STARTTRANSACTION;#获取A的余额存入A_balance变量:60SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='B'forUPDATE;执行事务1中剩余的sql#获取B的余额并存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='B'forUPDATE;#修改A的余额UPDATEaccountsetbalance=@A_balance-50其中user_id='A';#修改B的余额UPDATEaccountsetbalance=@B_balance+50whereuser_id='B';COMMIT;可以看出在事务1中获取B的数据的写锁时发生了超时,为什么会这样呢?主要原因是我们在第2步中已经获取了事务2中B数据的写锁,所以在事务2提交或回滚之前,事务1永远不会获取到B数据的写锁。执行事务2中剩余的sql#获取A的余额,存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='A'forUPDATE;#修改B的余额UPDATEaccountsetbalance=@A_balance-30whereuser_id='B';#修改AUPDATE账户余额setbalance=@B_balance+30whereuser_id='A';犯罪;同样,在事务2中获取A数据的写锁时,出现了超时情况。因为在步骤1的事务1中已经获取了A数据的写锁,那么在事务1提交或者回滚之前,事务2永远不会获得A数据的写锁。为什么会这样?主要原因是事务1和事务2在等待对方获取锁的过程中,导致两个事务都挂了阻塞,最后抛出获取锁超时的异常。3.5死锁带来的问题众所周知,数据库连接资源是非常宝贵的。如果一个连接因为事务阻塞而长时间没有释放,那么新请求要执行的SQL也会排队,越积越多,最终会延迟。使整个应用程序崩溃。一旦你的应用部署在没有进行熔断处理的微服务系统中,由于整个链路被阻塞,就会触发雪崩效应,导致严重的生产事故。4.如何解决死锁问题?要解决死锁问题,我们可以从死锁的四个必要条件入手。由于资源独占条件和非剥夺条件是锁本质的功能体现,是不可修改的,所以我们尝试从另外两个条件来解决。4.1打破请求和持有条件根据上面的定义,出现这种情况是因为事务1和事务2同时竞争锁A和锁B,那么我们能不能保证锁A和锁B只能竞争和持有一次一笔交易?羊毛布?答案是肯定的。我们来看看下面的伪代码:/***交易1入口(A,B)*交易2入口(B,A)**/publicvoidtransferAccounts(StringuserFrom,StringuserTo){//获取分布输入锁Locklock=Redisson.getLock();//启动事务JDBC.excute("STARTTRANSACTION;");//执行转账sqlJDBC.excute("#获取A的余额存入A_balance变量:80\n"+"SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='"+userFrom+"'forUPDATE;\n"+"#获取B的余额并将其存储在B_balance变量中:60\n"+"SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='"+userTo+"'forUPDATE;\n"+"\n"+"#修改A的余额\n"+"UPDATEaccountsetbalance=@A_balance-50whereuser_id='"+userFrom+"';\n"+"#修改B的余额\n"+"UPDATEaccountsetbalance=@B_balance+50whereuser_id='"+userTo+"';\n");//提交事务JDBC.excute("COMMIT;");//释放锁lock.unLock();}上面的伪代码显然可以解决死锁问题,因为所有的事务都是通过分布式锁串行执行的难道真的==一切都会好的==?在小流量的情况下貌似没问题,但是在==高并发场景下====整个服务的性能瓶颈==,因为即使你部署的机器再多,但是由于==分布的原因lock==,你的业务只能串行进行。服务性能并没有因为集群部署而增加并发,无法满足分布式业务快速、准确、稳定的要求。所以我们不妨换种方式看看如何解决死锁问题。4.2打破相互获取锁的条件(推荐)打破这个条件其实很简单,就是保证事务按顺序获取锁就可以了,即总是先获取锁A再获取锁B.我们来看看如何优化前面的伪代码?/***交易1入口(A,B)*交易2入口(B,A)**/publicvoidtransferAccounts(StringuserFrom,StringuserTo){//对用户A和B进行排序,使得userFrom总是对于用户A,userTo始终是用户Bif(userFrom.hashCode()>userTo.hashCode()){Stringtmp=userFrom;userFrom=userTo;userTo=tmp;}//开始事务JDBC.excute("STARTTRANSACTION;");//执行转账sqlJDBC.excute("#获取A的余额存入A_balance变量:80\n"+"SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='"+userFrom+"'forUPDATE;\n"+"#获取B的余额并将其存储在B_balance变量中:60\n"+"SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='"+userTo+"'forUPDATE;\n"+"\n"+"#修改A的余额\n"+"UPDATEaccountsetbalance=@A_balance-50whereuser_id='"+userFrom+"';\n"+"#修改B的余额\n"+"UPDATEaccountsetbalance=@B_balance+50whereuser_id='"+userTo+"';\n");//提交交易JDBC.excute("COMMIT;");}假设交易事务1的入参为(A,B),事务2的入参为(B,A),由于我们已经对这两个用户参数进行了排序,所以在事务1中,需要先获取锁A,再获取锁A锁B,和事务2也是一样先获取锁A再获取锁B,两个事务都是顺序获取锁的,所以互获取锁的条件被打破,最终完美解决了死锁问题5.总结因为mysql在网上应用广泛,所以死锁的问题经常被问到,希望兄弟们能够掌握这方面的知识,提高自己的竞争力,最后出去打工也不容易,希望各位兄弟可以找到自己喜欢的工作,兄弟们可以==关注,点赞,收藏,评论==支持一波,非常感谢!