• MySQL死锁举例及代码如何解决


    什么是死锁

    死锁指的是在两个或两个以上不同的进程或线程中,由于存在共同资源的竞争或进程(或线程)间的通讯而导致各个线程间相互挂起等待,如果没有外力作用,最终会引发整个系统崩溃。

    Mysql出现死锁的必要条件

    资源独占条件

    指多个事务在竞争同一个资源时存在互斥性,即在一段时间内某资源只由一个事务占用,也可叫独占资源(如行锁)。

    请求和保持条件

    指在一个事务a中已经获得锁A,但又提出了新的锁B请求,而该锁B已被其它事务b占有,此时该事务a则会阻塞,但又对自己已获得的锁A保持不放。

    不剥夺条件

    指一个事务a中已经获得锁A,在未提交之前,不能被剥夺,只能在使用完后提交事务再自己释放。

    相互获取锁条件

    指在发生死锁时,必然存在一个相互获取锁过程,即持有锁A的事务a在获取锁B的同时,持有锁B的事务b也在获取锁A,最终导致相互获取而各个事务都阻塞。

    Mysql经典死锁案例

    假设存在一个转账情景,A账户给B账户转账50元的同时,B账户也给A账户转账30元,那么在这过程中是否会存在死锁情况呢?

    建表语句

    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='账户余额表';
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    初始化相关数据

    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);
    
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    正常转账过程

    在说死锁问题之前,咱们先来看看正常的转账过程。
    正常情况下,A用户给B用户转账50元,可在一个事务内完成,需要先获取A用户的余额和B用户的余额,因为之后需要修改这两条数据,所以需要通过写锁(for UPDATE)锁住他们,防止其他事务更改导致我们的更改丢失而引起脏数据。
    相关sql如下:
    开启事务之前需要先把mysql的自动提交关闭

    set autocommit=0;
    # 查看事务自动提交状态状态
    show VARIABLES like 'autocommit';
    
    • 1
    • 2
    • 3
    # 转账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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    执行后的结果:
    在这里插入图片描述
    可以看到数据更新都是正常的情况

    死锁转账过程

    初始化的余额为:
    在这里插入图片描述
    假设在高并发情况下存在这种场景,A用户给B用户转账50元的同时,B用户也给A用户转账30元。

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

    • 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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 在事务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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    • 在事务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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    同理可得,在事务2中获取A数据的写锁时也出现了超时情况。因为步骤1的时候已经在事务1中获取到A数据的写锁了,那么在事务1提交或回滚前事务2永远都拿不到A数据的写锁。

    • 为什么会出现这种情况呢?
      主要是因为事务1和事务2存在相互等待获取锁的过程,导致两个事务都挂起阻塞,最终抛出获取锁超时的异常。
      在这里插入图片描述

    死锁导致的问题

    众所周知,数据库的连接资源是很珍贵的,如果一个连接因为事务阻塞长时间不释放,那么后面新的请求要执行的sql也会排队等待,越积越多,最终会拖垮整个应用。一旦你的应用部署在微服务体系中而又没有做熔断处理,由于整个链路被阻断,那么就会引发雪崩效应,导致很严重的生产事故。

    如何代码层面解决死锁问题?

    要想解决死锁问题,我们可以从死锁的四个必要条件入手。
    由于资源独占条件和不剥夺条件是锁本质的功能体现,无法修改,所以咱们从另外两个条件尝试去解决。

    打破请求和保持条件

    根据上面定义可知,出现这个情况是因为事务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();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

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

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

    打破相互获取锁条件(推荐)

    要打破这个条件其实也很简单,那就是事务再获取锁的过程中保证顺序获取即可,也就是锁A始终在锁B之前获取。
    我们来看看之前的伪代码怎么优化?

    /**
    * 事务1入参(A, B)
    * 事务2入参(B, A)
    **/
    public void transferAccounts(String userFrom, String userTo) {
         // 对用户A和B进行排序,让userFrom始终为用户A,userTo始终为用户B
         int flag = 1;
         if (userFrom.hashCode() > userTo.hashCode()) {
             String tmp = userFrom;
             userFrom = userTo;
             userTo = tmp;
             flag = -1;
         }
         // 开启事务
         JDBC.excute("START TRANSACTION;");
         // 执行转账sql
         JDBC.excute("# 获取userFrom  的余额并存入A_balance变量:80\n" +
                 "SELECT user_id,@A_balance:=balance from account where user_id = '" + userFrom + "' for UPDATE;\n" +
                 "# 获取userTo  的余额并存入B_balance变量:60\n" +
                 "SELECT user_id,@B_balance:=balance from account where user_id = '" + userTo + "' for UPDATE;\n" +
                 "\n" +
                 "# 修改userFrom  的余额\n" +
                 "UPDATE account set balance = @A_balance - " + (flag * 50) + " where user_id = '" + userFrom + "';\n" +
                 "# 修改userTo  的余额\n" +
                 "UPDATE account set balance = @B_balance + " + (flag * 50) + " where user_id = '" + userTo + "';\n");
         // 提交事务
         JDBC.excute("COMMIT;");
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    假设事务1的入参为(A, B),事务2入参为(B, A),由于我们对两个用户参数进行了排序,所以在事务1中需要先获取锁A在获取锁B,事务2也是一样要先获取锁A在获取锁B,两个事务都是顺序获取锁,所以也就打破了相互获取锁的条件,最终完美解决死锁问题。

    SQL层面解决经验分享

    问题描述

    两个想通的sql,都在in一个普通索引的值。比如:
    表A中普通索引编号num列
    sql1: select * from A where num in(1,2,3,5,7)
    sql2: select * from A where num in(7,6,2,9,1)

    在并发情况下偶发了一次死锁。

    出现死锁的原因

    mysql有自己的sql语句优化器,在概率极低的情况下将where子句后面的条件优化成一模一样的情况了。
    But,in条件里的值是不会优化的,所有就造成类似于上面举例中的场景了。

    解决方案分享

    1、num考虑作为唯一主键
    2、将in后面的条件进行排序

  • 相关阅读:
    2022年中职组网络安全新赛题
    TypeScript 笔记:String 字符串
    多重背包问题 ← 规模小时可转化为0-1背包问题
    【Java刷题进阶】基础入门篇⑥
    【esp32-adf】pipeline源码分析
    学习学习之高效学习
    每日一问07——什么是Softmax回归?和线性回归的区别是什么?
    JDK 8新特性-Lambda表达式
    程序人生——Java中类、对象及方法的建议(1)
    学生个人网页模板 学生个人网页设计作品 简单个人主页成品 个人网页制作 HTML学生个人网站作业设计代做
  • 原文地址:https://blog.csdn.net/qq_33333654/article/details/126949316