本教程操作环境:windows7系统、mysql8版本、Dell G3电脑。
1、什么是死锁?
2、Mysql出现死锁的必要条件
资源独占条件
请求和保持条件
不剥夺条件
相互获取锁条件
3、 Mysql经典死锁案例
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 正常转账过程
开启事务之前需要先把mysql的自动提交关闭
set autocommit=0; # 查看事务自动提交状态状态
show VARIABLES like 'autocommit';
# 转账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 死锁转账过程
初始化的余额为:

那么我们的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;
2.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;
3.在事务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数据的写锁。
4.在事务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;

5. 为什么会出现这种情况呢?

3.5 死锁导致的问题
4、如何解决死锁问题?
4.1 打破请求和保持条件
/**
* 事务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 打破相互获取锁条件(推荐)
/**
* 事务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;");
}
5、 如何预防死锁
阻止死锁的途径就是避免满足死锁条件的情况发生,为此我们在开发的过程中需要遵循如下原则:
死锁的概念:
死锁的常见表现:
产生死锁的原因:
死锁的四个必要条件(四个条件四者不可缺一):
解除死锁的两种方法:
6、死锁场景
本文死锁场景皆为工作中遇到(或同事遇到)并解决的死锁场景,写这篇文章的目的是整理和分享,欢迎指正和补充,本文死锁场景包括:
注:以下场景隔离级别均为默认的Repeatable Read;
1)行锁导致死锁

死锁原因详解:
相应业务案例和解决方案:
该场景常见于事务中存在for循环更新某条记录的情况,死锁日志显示lock_mode X locks rec but not gap waiting(即行锁而非间隙锁),解决方案:
2)gap lock/next keys lock导致死锁



死锁原因分析:
解决方案:
3)index merge导致死锁
t_user结构改造为:



死锁分析:
1、如果两个事务上锁的顺序相反,则有一定的概率出现死锁。另外,index_merge的形式锁住了很多不符合条件的行,浪费了资源。一般死锁日志打印的信息为:
lock_mode X locks rec but not gap waiting Record lock
解决方案:创建联合索引,使执行计划只会用到一个索引。
注:
update table set name = “wea” where col_1 = 1 or col_2 = 2 ;
col_1和col_2为联合索引,遵循最左原则col_1会走索引,但col_2会对整个索引进行扫描,此时会对整个索引加锁。
【相关推荐:mysql视频教程】
