您好,欢迎访问一九零五行业门户网

什么是死锁?聊聊对MySQL死锁的理解

什么是死锁?下面本篇文章带大家了解一下mysql死锁。聊聊mysql出现死锁的必要条件,如何解决死锁问题?希望对大家有所帮助。
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 建表语句
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)
# 转账sqlstart transaction;# 获取a 的余额并存入a_balance变量:80select user_id,@a_balance:=balance from account where user_id = 'a' for update;# 获取b 的余额并存入b_balance变量:60select 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这条数据。
# 事务1set autocommit=0;start transaction;# 获取a 的余额并存入a_balance变量:80select user_id,@a_balance:=balance from account where user_id = 'a' for update;
2、b用户给a用户转账30元,需在程序中开启事务2来执行sql,并获取b的余额同时锁住b这条数据。
# 事务2set autocommit=0;start transaction;# 获取a 的余额并存入a_balance变量:60select user_id,@a_balance:=balance from account where user_id = 'b' for update;
3、在事务1中执行剩下的sql
# 获取b 的余额并存入b_balance变量:60select 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变量:60select 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数据的写锁。
5、 为什么会出现这种情况呢?
主要是因为事务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在互联网中的大量使用,所以死锁问题还是经常会被问到,希望兄弟们能掌握这方面的知识,提高自己的竞争力。
【相关推荐:mysql视频教程】
以上就是什么是死锁?聊聊对mysql死锁的理解的详细内容。
其它类似信息

推荐信息