it编程 > 数据库 > Mysql

Mysql如何解决死锁问题

18人参与 2025-04-23 Mysql

【一】mysql中锁分类和加锁情况

【1】按锁的粒度分类

全局锁

加锁情况:使用 flush tables with read lock 语句,它会对整个数据库实例加锁,使整个数据库处于只读状态。常用于全量备份等场景,确保备份期间数据的一致性。

示例:

flush tables with read lock;
-- 进行备份操作
unlock tables;

表级锁

1、表共享读锁(table read lock)

特点

加锁情况

示例:

-- 会话 1
lock tables users read;
select * from users;
-- 若尝试写入,会报错
-- update users set name = 'new_name' where id = 1; 
unlock tables;

-- 会话 2
select * from users; -- 可以正常读取

2、表独占写锁(table write lock)

特点

加锁情况

示例:

-- 会话 1
lock tables users write;
select * from users;
update users set name = 'new_name' where id = 1;
unlock tables;

-- 会话 2
-- 若在会话 1 持有写锁期间尝试读写,会被阻塞
select * from users; 

3、元数据锁(mdl)

特点:

加锁情况:

-- 会话 1
start transaction;
select * from users; -- 自动加共享 mdl 锁
-- 此时会话 2 可以进行读操作,但不能进行表结构修改

-- 会话 2
-- 可以正常读取
select * from users; 
-- 若执行 alter table 会被阻塞
-- alter table users add column new_column varchar(255); 

-- 会话 1 提交事务释放共享 mdl 锁
commit;

3、意向锁(intention lock)

加锁方式:

特点:

示例

-- 会话 1
start transaction;
select * from users where id = 1 for update; -- 自动对表加意向排他锁
-- 会话 2 尝试对表加表级共享读锁会被阻塞
-- lock tables users read; 
commit;

行级锁

1、记录锁(record lock)

(1)定义

记录锁是对索引记录的锁定,也就是对表中某一行数据的索引项加锁。需要注意的是,记录锁总是会锁定索引记录,如果表没有设置索引,mysql 会自动创建一个隐藏的聚簇索引来使用。

(2)加锁情况

在可重复读或串行化隔离级别下,使用 select … for update 或 update、delete 等语句对满足条件的行记录加锁。例如:

select * from table_name where id = 1 for update;

执行 update、delete 语句时,也会对操作的行记录加记录锁。示例如下:

update users set name = 'john' where id = 1;
delete from users where id = 1;

2、间隙锁(gap lock)

(1)定义

间隙锁锁定的是索引记录之间的间隙,其目的在于防止其他事务在该间隙插入新记录,从而避免幻读问题。

(2)加锁情况

在可重复读隔离级别下,当使用范围查询(如 where id between 1 and 10)时,为了防止幻读,会对查询范围的间隙加锁。例如:

select * from table_name where id between 1 and 10 for update;

此语句不仅会对 id 在 1 到 10 之间的行记录加锁,还会对这些记录之间的间隙加锁,防止其他事务插入新的 id 在这个范围内的记录。

3、临键锁(next-key lock)

(1)定义

临键锁是记录锁和间隙锁的组合,它会锁定索引记录本身以及该记录前面的间隙。

(2)加锁情况

是记录锁和间隙锁的组合,在可重复读隔离级别下,对索引记录和其前面的间隙加锁。常用于范围查询和唯一性检查,防止幻读和插入异常。

select * from users where id > 10 for update;

这个语句会对 id 大于 10 的行记录及其前面的间隙加临键锁。

【2】按锁的模式分类

共享锁(s 锁)

加锁情况:使用 select … lock in share mode 语句对读取的行记录加共享锁,多个事务可以同时对同一行记录加共享锁,但不能同时加排他锁。例如:

select * from table_name where id = 1 lock in share mode;

排他锁(x 锁)

加锁情况:使用 select … for update、update、delete 等语句对操作的行记录加排他锁,一旦某事务对行记录加了排他锁,其他事务既不能读取也不能修改该行记录,直到排他锁被释放。

【二】加锁方式的影响因素

(1)隔离级别(innodb默认:可重复读-repeatable read)

不同的隔离级别对锁的使用和加锁范围有影响。

例如,可重复读隔离级别会使用间隙锁和临键锁来防止幻读,而读提交隔离级别则不会。

(2)查询语句

查询条件、索引使用情况等会影响加锁的范围和粒度。如果使用索引进行精确匹配,可能只对匹配的行记录加锁;如果是范围查询,可能会加间隙锁或临键锁。

(3)事务操作

不同的事务操作(如 select、insert、update、delete)会触发不同类型的锁。例如,insert 操作可能会对插入位置的间隙加锁,update 和 delete 操作会对操作的行记录加排他锁。

【三】mysql的死锁情况

【1】事务交叉更新导致死锁

情况描述

假设有两个事务 t1 和 t2,以及一个表 accounts 包含 id 和 balance 两列。

-- 事务 t1
start transaction;
update accounts set balance = balance + 100 where id = 1;
update accounts set balance = balance - 100 where id = 2;
commit;

-- 事务 t2
start transaction;
update accounts set balance = balance + 100 where id = 2;
update accounts set balance = balance - 100 where id = 1;
commit;

死锁原因

(1)t1 先对 id = 1 的行加排他锁,然后尝试对 id = 2 的行加排他锁;

(2)t2 先对 id = 2 的行加排他锁,然后尝试对 id = 1 的行加排他锁。

(3)此时,t1 等待 t2 释放 id = 2 的锁,而 t2 等待 t1 释放 id = 1 的锁,从而形成死锁。

【2】索引使用不当导致死锁

情况描述

有一个表 orders 包含 order_id 和 product_id 两列,product_id 上有索引。

-- 事务 t1
start transaction;
update orders set status = 'paid' where product_id = 1;
update orders set status = 'shipped' where product_id = 2;
commit;

-- 事务 t2
start transaction;
update orders set status = 'paid' where product_id = 2;
update orders set status = 'shipped' where product_id = 1;
commit;

死锁原因

由于 product_id 上有索引,更新操作会对索引记录和间隙加锁。t1 和 t2 按照不同的顺序对 product_id 进行更新,导致锁的获取顺序不一致,从而可能形成死锁。

【3】并发插入导致的死锁

情况描述

在可重复读隔离级别下,两个事务同时向一张有唯一索引的表中插入数据,且插入的数据在唯一索引列上有冲突。innodb 为了保证数据的一致性,会使用间隙锁,这可能导致死锁。

-- 事务t1
start transaction;
insert into unique_table (id, value) values (1, 'value1');

-- 事务t2
start transaction;
insert into unique_table (id, value) values (1, 'value2');

解决方案

可以考虑将隔离级别调整为读提交,但需要注意这可能会导致幻读问题。

或者在插入数据前,先进行唯一性检查,避免插入冲突的数据。

【4】外键约束引发的死锁

情况描述

有两张表,主表 a 和从表 b,从表 b 有外键关联到主表 a。当两个事务分别对主表和从表进行插入和删除操作时,由于外键约束的检查,可能会导致死锁。

示例代码:

-- 事务t1
start transaction;
insert into tablea (id, name) values (1, 'name1');
-- 假设这里有一些耗时的操作
delete from tableb where id = 1;

-- 事务t2
start transaction;
insert into tableb (id, a_id, value) values (1, 1, 'value1');
-- 假设这里有一些耗时的操作
delete from tablea where id = 1;

解决方案

确保在进行涉及外键关系的操作时,按照主表和从表的正确顺序进行操作,或者使用级联操作来简化事务中的操作,减少锁的竞争。

【5】⭐️删除不存在的数据导致间隙锁

情况描述

⭐️先delete,再insert,导致死锁

实例的日志记录表,实例在重跑的时候,会先根据instanceid去delete该实例关联的全部旧的记录信息,然后再陆续插入新的记录信息,instanceid有索引,出现锁超时的情况。在删除的时候根据实例id删除,但是记录可能不存在,如果删除的记录在数据库中存在,那么产生的就是普通的行锁;当删除的这条记录不存在,会在删除记录所在的区间加间隙锁。

背景信息

mysql版本:percona mysql server 5.7.19

隔离级别:可重复读(rr)

业务逻辑:并发下按某个索引字段先delete记录,再insert记录

begin;
delete from tb where order_id = xxx;
insert into tb(order_id) values(xxx);
commit;

mysql锁基本概念

锁模式兼容性表

死锁原因

打开参数,从innodb status获取更多的锁信息。

set global innodb_status_output_locks=on;

表结构:

 create table `tb` (
  `order_id` int(11) default null,
  key `idx_order_id` (`order_id`)
) engine=innodb default charset=utf8

表中数据:

mysql> select * from tb;
+----------+
| order_id |
+----------+
|       10 |
|       20 |
+----------+
2 rows in set (0.00 sec)

事务执行步骤:

(1)开启两个事务

(2)两个事务分别删除两个个不存在的记录

(3)两个事务分别插入该记录

当session1执行delete from tb where order_id=15;,由于条件order_id=15的记录不存在,session1 获得2个锁结构,分别是意向排他锁ix(表级锁)、gap锁(行级锁),如下:

---transaction 1055191443, active 20 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
mysql thread id 315642, os thread handle 139960342456064, query id 150462030 localhost root
table lock table `db`.`tb` trx id 1055191443 lock mode ix
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191443 lock_mode x locks gap before rec

当session2执行delete from tb where order_id=15;,同样由于order_id=15的记录不存在,session2 也获得2个锁结构,分别是意向排他锁ix(表级锁)、gap锁(行级锁),如下:

---transaction 1055191444, active 3 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
mysql thread id 315336, os thread handle 139960562685696, query id 150462412 localhost root
table lock table `db`.`tb` trx id 1055191444 lock mode ix
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191444 lock_mode x locks gap before rec

当session2执行insert into tb select 15;, session2 已经获取到ix锁,gap锁,等待 rec insert intention(插入意向锁)

---transaction 1055191444, active 68 sec inserting
mysql tables in use 1, locked 1
lock wait 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql thread id 315336, os thread handle 139960562685696, query id 150462778 localhost root executing
insert into tb select 15
------- trx has been waiting 2 sec for this lock to be granted:
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191444 lock_mode x locks gap before rec insert intention waiting
------------------
table lock table `db`.`tb` trx id 1055191444 lock mode ix
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191444 lock_mode x locks gap before rec
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191444 lock_mode x locks gap before rec insert intention waiting

当session1执行insert into tb select 15;,session1 已获取到ix锁,gap锁, 等待rec insert intention(插入意向锁), session1, session2 都在等待插入意向锁, 插入意向锁与gap锁冲突,双方都没有释放gap锁,又都在等待插入意向锁,死锁发生。

latest detected deadlock
------------------------
2018-11-03 17:15:11 0x7f4b0e7ea700
*** (1) transaction:
transaction 1055191444, active 135 sec inserting
mysql tables in use 1, locked 1
lock wait 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql thread id 315336, os thread handle 139960562685696, query id 150462778 localhost root executing
insert into tb select 15
*** (1) waiting for this lock to be granted:
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191444 lock_mode x locks gap before rec insert intention waiting
*** (2) transaction:
transaction 1055191443, active 201 sec inserting, thread declared inside innodb 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql thread id 315642, os thread handle 139960342456064, query id 150463172 localhost root executing
insert into tb select 15
*** (2) holds the lock(s):
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191443 lock_mode x locks gap before rec
*** (2) waiting for this lock to be granted:
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191443 lock_mode x locks gap before rec insert intention waiting
*** we roll back transaction (2)

案例扩展

以上死锁案例,业务代码逻辑是多线程并发下,有可能多个线程会执行相同order_id的job,比如两个线程执行的order_id 都是15。

另外一种情况,多个线程间,不会执行到相同order_id的情况,也可能发生死锁。比如一个线程order_id=15,另外一个线程order_id=16,如下所示:

锁情况与上述相同,不再赘述,死锁信息如下:

latest detected deadlock
------------------------
2018-11-03 17:28:30 0x7f4b0e667700
*** (1) transaction:
transaction 1055191450, active 18 sec inserting
mysql tables in use 1, locked 1
lock wait 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql thread id 316221, os thread handle 139960338228992, query id 150467652 localhost root executing
insert into tb select 16
*** (1) waiting for this lock to be granted:
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191450 lock_mode x locks gap before rec insert intention waiting
*** (2) transaction:
transaction 1055191449, active 28 sec inserting, thread declared inside innodb 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql thread id 316222, os thread handle 139960340870912, query id 150467681 localhost root executing
insert into tb select 15
*** (2) holds the lock(s):
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191449 lock_mode x locks gap before rec
*** (2) waiting for this lock to be granted:
record locks space id 1337 page no 4 n bits 72 index idx_order_id of table `db`.`tb` trx id 1055191449 lock_mode x locks gap before rec insert intention waiting
*** we roll back transaction (2)
 

解决方案

1-修改隔离级别为提交读(rc)

2-修改业务代码逻辑,删除记录之前,先select,确认该记录存在,再执行delete删除该记录。

【6】同一个事务中多条update修改同一条记录

情况描述

数据库是mysql 5.7,引擎是innodb,事务隔离级别是读提交(read-commited)。

死锁日志

transactions deadlock detected, dumping detailed information.2019-03-19t21:44:23.516263+08:00 5877341 [note] innodb: 
*** (1) transaction:transaction 173268495, active 0 sec fetching rowsmysql tables in use 1, locked 1lock wait 304 lock struct(s), heap size 41168, 6 row lock(s), undo log entries 1mysql thread id 5877358, os thread handle 47356539049728, query id 557970181 11.183.244.150 fin_instant_app updating
update `fund_transfer_stream` set `gmt_modified` = now(), `state` = 'processing' where ((`state` = 'new') and (`seller_id` = '38921111') and (`fund_transfer_order_no` = '99010015000805619031958363857'))2019-03-19t21:44:23.516321+08:00 5877341 [note] innodb: 
*** (1) holds the lock(s):record locks space id 173 page no 13726 n bits 248 index idx_seller_transno of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode x locks rec but not gaprecord lock, heap no 168 physical record: n_fields 3; compact format; info bits 0
2019-03-19t21:44:23.516565+08:00 5877341 [note] innodb: 
*** (1) waiting for this lock to be granted:record locks space id 173 page no 12416 n bits 128 index primary of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode x locks rec but not gap waitingrecord lock, heap no 56 physical record: n_fields 17; compact format; info bits 02019-03-19t21:44:23.517793+08:00 5877341 [note] innodb: 
*** (2) transaction:transaction 173268500, active 0 sec fetching rows, thread declared inside innodb 81mysql tables in use 1, locked 1302 lock struct(s), heap size 41168, 2 row lock(s), undo log entries 1mysql thread id 5877341, os thread handle 47362313119488, query id 557970189 11.131.81.107 fin_instant_app updating
update `fund_transfer_stream_0056` set `gmt_modified` = now(), `state` = 'processing' where ((`state` = 'new') and (`seller_id` = '38921111') and (`fund_transfer_order_no` = '99010015000805619031957477256'))2019-03-19t21:44:23.517855+08:00 5877341 [note] innodb: 
*** (2) holds the lock(s):record locks space id 173 page no 12416 n bits 128 index primary of table `fin_instant_0003`.`fund_transfer_stream_0056` trx id 173268500 lock_mode x locks rec but not gaprecord lock, heap no 56 physical record: n_fields 17; compact format; info bits 0
2019-03-19t21:44:23.519053+08:00 5877341 [note] innodb: 
*** (2) waiting for this lock to be granted:record locks space id 173 page no 13726 n bits 248 index idx_seller_transno of table `fin_instant_0003`.`fund_transfer_stream_0056` trx id 173268500 lock_mode x locks rec but not gap waitingrecord lock, heap no 168 physical record: n_fields 3; compact format; info bits 0
2019-03-19t21:44:23.519297+08:00 5877341 [note] innodb: *** we roll back transaction (2)

定位导致死锁的两条sql

update `fund_transfer_stream_0056` set `gmt_modified` = now(), `state` = 'processing' where ((`state` = 'new') and (`seller_id` = '38921111') and (`fund_transfer_order_no` = '99010015000805619031957477256'))

update `fund_transfer_stream_0056` set `gmt_modified` = now(), `state` = 'processing' where ((`state` = 'new') and (`seller_id` = '38921111') and (`fund_transfer_order_no` = '99010015000805619031958363857'))

索引情况如下

key `idx_seller` (`seller_id`),
key `idx_seller_transno` (`seller_id`,`fund_transfer_order_no`(20))

(1)事务1,持有索引idx_seller_transno的锁,在等待获取primary的锁。

(2)事务2,持有primary的锁,在等待获取idx_seller_transno的锁。

(3)因事务1和事务2之间发生循环等待,故发生死锁。

事务1和事务2当前持有的锁均为: lock_mode x locks rec but not gap ,两个事务对记录加的都是x 锁,no gap锁,即对当行记录加锁,并未加间隙锁。

死锁原因

首先,此次死锁一定是和gap锁以及next-key lock没有关系的。因为我们的数据库隔离级别是读提交(read-commited)的,这种隔离级别是不会添加gap锁的,gap锁只有在读未提交会用。前面的死锁日志也提到这一点。

翻看代码

@transactional(rollbackfor = exception.class)public int doprocessing(string sellerid, long id, string fundtransferorderno) {    
    fundtreansferstreamdao.updatefundstreamid(sellerid, id, fundtransferorderno);
    return fundtreansferstreamdao.updatestatus(sellerid, fundtransferorderno, fundtransferstreamstate.processing.name());
}

该代码的目的是先后修改同一条记录的两个不同字段,updatefundstreamid sql:

update fund_transfer_stream        set gmt_modified=now(),fund_transfer_order_no = #{fundtransferorderno}        where id = #{id} and seller_id = #{sellerid}
update fund_transfer_stream    set gmt_modified=now(),state = #{state}    where fund_transfer_order_no = #{fundtransferorderno} and seller_id = #{sellerid}    and state = 'new'

可以看到,我们的同一个事务中执行了两条update语句,这里分别查看下两条sql的执行计划:

updatefundstreamid执行的时候使用到的是primary索引。

updatestatus执行的时候使用到的是idx_seller_transno索引。

主要问题出在我们的idx_seller_transno索引上面

索引创建语句中,我们使用了前缀索引,为了节约索引空间,提高索引效率,我们只选择了fund_transfer_order_no字段的前20位作为索引值。

因为fund_transfer_order_no只是普通索引,而非唯一性索引。又因为在一种特殊情况下,会有同一个用户的两个fund_transfer_order_no的前20位相同,这就导致两条不同的记录的索引值一样(因为seller_id 和fund_transfer_order_no(20)都相同 )。

就如本文中的例子,发生死锁的两条记录的fund_transfer_order_no字段的值:99010015000805619031958363857和99010015000805619031957477256这两个就是前20位相同的。

原因汇总

在mysql中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,mysql就会锁定这条主键索引;如果一条语句操作了非主键索引,mysql会先锁定该非主键索引,再锁定相关的主键索引。

(1)事务1执行update1占用primary = 1的锁

(2)事务2执行update1 占有primary = 2的锁;

(3)事务1执行update2占有idx_seller_transno = (3111095611,99010015000805619031)的锁,尝试占有primary = 2锁失败(阻塞);

(4)事务2执行update2尝试占有idx_seller_transno = (3111095611,99010015000805619031)的锁失败(死锁);

解决方案

(1)修改索引:只要我们把前缀索引 idx_seller_transno中fund_transfer_order_no的前缀长度修改下就可以了。比如改成50。即可避免死锁。

(2)解决办法就是改代码

所有update都通过主键id进行。

在同一个事务中,避免出现多条update语句修改同一条记录。

【四】排查线上死锁问题

查看死锁日志

mysql 会将死锁信息记录在错误日志中,可以通过查看错误日志找到死锁的详细信息,包括死锁发生的时间、涉及的事务和 sql 语句等。

使用 show engine innodb status 命令

该命令可以显示 innodb 存储引擎的状态信息,其中包含最近一次死锁的详细信息,如死锁的事务 id、持有和等待的锁等。

show engine innodb status;

开启 innodb_print_all_deadlocks 参数

将该参数设置为 on,可以让 mysql 记录所有的死锁信息到错误日志中,方便后续分析。

set global innodb_print_all_deadlocks = on;

【五】解决死锁问题

1、优化事务逻辑

确保事务按照相同的顺序访问资源,避免交叉更新。例如,将上述事务 t1 和 t2 都按照 id 从小到大的顺序进行更新:

-- 事务 t1
start transaction;
update accounts set balance = balance + 100 where id = 1;
update accounts set balance = balance - 100 where id = 2;
commit;


-- 事务 t2
start transaction;
update accounts set balance = balance + 100 where id = 1;
update accounts set balance = balance - 100 where id = 2;
commit;

2、减少事务持有锁的时间

尽量缩短事务的执行时间,减少锁的持有时间,降低死锁的概率。例如,将大事务拆分成多个小事务。

3、调整隔离级别

如果业务允许,可以将隔离级别从可重复读调整为读提交,减少间隙锁和临键锁的使用,降低死锁的可能性。

set session transaction isolation level read committed;

4、优化索引

确保 sql 语句使用合适的索引,避免全表扫描和范围扫描,减少锁的范围和粒度。例如,为经常用于查询和更新的列添加索引。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

Mysql用户授权(GRANT)语法及示例解读

04-23

Mysql启动报错Error1045(28000)的原因分析及解决

04-23

MySQL线上大表DDL避免锁表的解决方案

04-23

MySQL的执行原理之 MySQL的查询重写规则详解

04-23

mysql中的group by高级用法

04-23

Mysql 主从集群同步延迟的问题解决

04-24

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论