关于mysql:阿里一面说说你对Mysql死锁的理解

3次阅读

共计 5056 个字符,预计需要花费 13 分钟才能阅读完成。

又到了金三银四的时候,大家都按耐不住心田的躁动,我在这里给大家分享下之前面试中遇到的一个知识点(死锁问题),如有有余,欢送大佬们指导指导。

1、什么是死锁?

死锁指的是在两个或两个以上不同的过程或线程中,因为存在独特资源的竞争或过程(或线程)间的通信而导致各个线程间互相挂起期待,如果没有外力作用,最终会引发整个零碎解体。

2、Mysql 呈现死锁的必要条件

  1. 资源独占条件

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

  1. 申请和放弃条件

指在一个事务 a 中曾经取得锁 A,但又提出了新的锁 B 申请,而该锁 B 已被其它事务 b 占有,此时该事务 a 则会阻塞,但又对本人已取得的锁 A 放弃不放。

  1. 不剥夺条件

指一个事务 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_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ZqQIOmjjg==,size_20,color_FFFFFF,t_70,g_se,x_16)
# 转账 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;

执行后的后果:

能够看到数据更新都是失常的状况

3.4 死锁转账过程

初始化的余额为:

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

那么咱们的 java 程序操作的过程和工夫线如下:

  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;
  1. 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. 在事务 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 数据的写锁。

  1. 在事务 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 数据的写锁。

  1. 为什么会呈现这种状况呢?

次要是因为事务 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 在互联网中的大量应用,所以死锁问题还是常常会被问到,心愿兄弟们能把握这方面的常识,进步本人的竞争力。

最初,外出打工不易,心愿各位兄弟找到本人心仪的工作,虎年发发发!
也心愿兄弟们能 == 关注、点赞、珍藏、评论 == 反对一波,非常感谢大家!

正文完
 0