当前位置: 首页 > MySQL数据库

MySQL死锁是什么及怎么掌握

时间:2026-01-27 10:37:58

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 建表语句

        CREATETABLE`account`(`id`int(11)NOTNULLCOMMENT'主键',`user_id`varchar(56)NOTNULLCOMMENT'用户id',`balance`float(10,2)DEFAULTNULLCOMMENT'余额',PRIMARYKEY(`id`),UNIQUEKEY`idx_user_id`(`user_id`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='账户余额表';

        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用户的余额,因为之后需要修改这两条数据,所以需要通过写锁(for UPDATE)锁住他们,防止其他事务更改导致我们的更改丢失而引起脏数据。
        相关sql如下:

        开启事务之前需要先把mysql的自动提交关闭

        setautocommit=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)

        #转账sqlSTARTTRANSACTION;#获取A的余额并存入A_balance变量:80SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='A'forUPDATE;#获取B的余额并存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='B'forUPDATE;#修改A的余额UPDATEaccountsetbalance=@A_balance-50whereuser_id='A';#修改B的余额UPDATEaccountsetbalance=@B_balance+50whereuser_id='B';COMMIT;

        执行后的结果:

        可以看到数据更新都是正常的情况

        3.4 死锁转账过程

        初始化的余额为:

        假设在高并发情况下存在这种场景,A用户给B用户转账50元的同时,B用户也给A用户转账30元。

        那么我们的java程序操作的过程和时间线如下:

        1、A用户给B用户转账50元,需在程序中开启事务1来执行sql,并获取A的余额同时锁住A这条数据。

        #事务1setautocommit=0;STARTTRANSACTION;#获取A的余额并存入A_balance变量:80SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='A'forUPDATE;

        2、B用户给A用户转账30元,需在程序中开启事务2来执行sql,并获取B的余额同时锁住B这条数据。

        #事务2setautocommit=0;STARTTRANSACTION;#获取A的余额并存入A_balance变量:60SELECTuser_id,@A_balance:=balancefromaccountwhereuser_id='B'forUPDATE;

        3、在事务1中执行剩下的sql

        #获取B的余额并存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='B'forUPDATE;#修改A的余额UPDATEaccountsetbalance=@A_balance-50whereuser_id='A';#修改B的余额UPDATEaccountsetbalance=@B_balance+50whereuser_id='B';COMMIT;

        可以看到,在事务1中获取B数据的写锁时出现了超时情况。为什么会这样呢?主要是因为我们在步骤2的时候已经在事务2中获取到B数据的写锁了,那么在事务2提交或回滚前事务1永远都拿不到B数据的写锁。

        4、在事务2中执行剩下的sql

        #获取A的余额并存入B_balance变量:60SELECTuser_id,@B_balance:=balancefromaccountwhereuser_id='A'forUPDATE;#修改B的余额UPDATEaccountsetbalance=@A_balance-30whereuser_id='B';#修改A的余额UPDATEaccountsetbalance=@B_balance+30whereuser_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)**/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();}

        上面的伪代码显而易见可以解决死锁问题,因为所有的事务都是通过分布式锁来串行执行的。

        那么这样就真的万事大吉了吗?

        在小流量情况下看起来是没问题的,但是在高并发场景下这里将成为整个服务的性能瓶颈,因为即使你部署了再多的机器,但由于分布式锁的原因,你的业务也只能串行进行,服务性能并不因为集群部署而提高并发量,完全无法满足分布式业务下快、准、稳的要求,所以咱们不妨换种方式来看看怎么解决死锁问题。

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


        上一篇:MySQL中超键、主键及候选键的区别是什么
        下一篇:MySQL安装常见报错怎么处理
        mysql

  • 英特尔与 Vertiv 合作开发液冷 AI 处理器
  • 英特尔第五代 Xeon CPU 来了:详细信息和行业反应
  • 由于云计算放缓引发扩张担忧,甲骨文股价暴跌
  • Web开发状况报告详细介绍可组合架构的优点
  • 如何使用 PowerShell 的 Get-Date Cmdlet 创建时间戳
  • 美光在数据中心需求增长后给出了强有力的预测
  • 2027服务器市场价值将接近1960亿美元
  • 生成式人工智能的下一步是什么?
  • 分享在外部存储上安装Ubuntu的5种方法技巧
  • 全球数据中心发展的关键考虑因素
  • 英特尔与 Vertiv 合作开发液冷 AI 处理器

    英特尔第五代 Xeon CPU 来了:详细信息和行业反应

    由于云计算放缓引发扩张担忧,甲骨文股价暴跌

    Web开发状况报告详细介绍可组合架构的优点

    如何使用 PowerShell 的 Get-Date Cmdlet 创建时间戳

    美光在数据中心需求增长后给出了强有力的预测

    2027服务器市场价值将接近1960亿美元

    生成式人工智能的下一步是什么?

    分享在外部存储上安装Ubuntu的5种方法技巧

    全球数据中心发展的关键考虑因素