数据库出现死锁了。还不知道什么原因引起的?快来看看吧!

死锁的本质

两个事务同时操作同样的数据,然后互相等待对方释放锁。

MySQL死锁的检测与处理

sql 复制代码
-- 查看死锁相关配置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';  -- 锁等待超时时间,默认50秒
SHOW VARIABLES LIKE 'innodb_deadlock_detect';    -- 死锁检测,默认ON
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks'; -- 打印所有死锁到错误日志

-- 查看最近死锁信息
SHOW ENGINE INNODB STATUS\G;  -- 查看 LATEST DETECTED DEADLOCK 部分

常见的死锁场景与示例

一、不同顺序访问相同记录

ini 复制代码
-- 死锁示例:不同的事务以不同的顺序更新相同的记录
-- 事务1
START TRANSACTION;
-- 先更新id=1,获取锁
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 模拟一些其他操作
DO SLEEP(1);
-- 再更新id=2
UPDATE users SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 同时,事务2
START TRANSACTION;
-- 先更新id=2,获取锁(与事务1顺序相反)
UPDATE users SET balance = balance - 200 WHERE id = 2;
-- 模拟一些其他操作
DO SLEEP(1);
-- 再更新id=1(等待事务1释放锁)
UPDATE users SET balance = balance + 200 WHERE id = 1;
COMMIT;

-- 结果:死锁发生!

二、间隙锁(Gap Lock)导致的死锁

sql 复制代码
-- 间隙锁死锁示例
-- 事务1
START TRANSACTION;
-- 在 order_no='ORD002' 上获取间隙锁(锁住 ORD001 和 ORD003 之间的间隙)
SELECT * FROM orders WHERE order_no = 'ORD002' FOR UPDATE;
-- 在间隙中插入
INSERT INTO orders(order_no, amount, status) VALUES ('ORD002', 150, 'PENDING');

-- 事务2
START TRANSACTION;
-- 同样在 order_no='ORD004' 上获取间隙锁
SELECT * FROM orders WHERE order_no = 'ORD004' FOR UPDATE;
-- 插入到间隙
INSERT INTO orders(order_no, amount, status) VALUES ('ORD004', 250, 'PENDING');

-- 事务1尝试插入 'ORD004'
INSERT INTO orders(order_no, amount, status) VALUES ('ORD004', 250, 'PENDING');

-- 事务2尝试插入 'ORD002'
INSERT INTO orders(order_no, amount, status) VALUES ('ORD002', 150, 'PENDING');

-- 结果:死锁!两个事务互相等待对方释放间隙锁

三、Next-Key Lock死锁

sql 复制代码
-- 死锁示例
-- 事务1
START TRANSACTION;
-- 锁定 category='B' 且 price>=150 的所有记录和间隙
SELECT * FROM products 
WHERE category = 'B' AND price >= 150 
FOR UPDATE;
-- 在间隙中插入
INSERT INTO products VALUES (6, 'B', 180);

-- 事务2
START TRANSACTION;
-- 也锁定 category='B' 且 price>=150 的所有记录和间隙
SELECT * FROM products 
WHERE category = 'B' AND price >= 150 
FOR UPDATE;
-- 在间隙中插入
INSERT INTO products VALUES (7, 'B', 220);

-- 两个事务都试图在对方锁定的间隙中插入,造成死锁

四、外键约束导致的死锁

sql 复制代码
CREATE TABLE parent (
    id INT PRIMARY KEY,
    name VARCHAR(50)
) ENGINE=InnoDB;

CREATE TABLE child (
    id INT PRIMARY KEY,
    parent_id INT,
    value VARCHAR(50),
    FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE
) ENGINE=InnoDB;

INSERT INTO parent VALUES (1, 'Parent1'), (2, 'Parent2');
INSERT INTO child VALUES (1, 1, 'Child1'), (2, 1, 'Child2'), (3, 2, 'Child3');

-- 外键死锁示例
-- 事务1
START TRANSACTION;
-- 删除父表记录,会在子表相关记录上加锁
DELETE FROM parent WHERE id = 1;
-- 子表现在有事务1的锁

-- 事务2
START TRANSACTION;
-- 插入子表记录,需要检查外键约束
INSERT INTO child VALUES (4, 2, 'Child4');
-- 在父表id=2上加共享锁
-- 然后尝试删除父表记录
DELETE FROM parent WHERE id = 2;

-- 事务1
-- 尝试插入子表记录
INSERT INTO child VALUES (5, 2, 'Child5');

-- 结果:事务1等待事务2释放父表的锁,事务2等待事务1释放子表的锁 → 死锁

五、锁升级导致的死锁

sql 复制代码
-- 准备大数据量表
CREATE TABLE large_table (
    id INT PRIMARY KEY AUTO_INCREMENT,
    data VARCHAR(100),
    status TINYINT,
    INDEX idx_status(status)
) ENGINE=InnoDB;

-- 插入测试数据
DELIMITER $$
CREATE PROCEDURE insert_test_data()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i <= 10000 DO
        INSERT INTO large_table(data, status) 
        VALUES (CONCAT('Data-', i), MOD(i, 3));
        SET i = i + 1;
    END WHILE;
END
$$
DELIMITER ;

CALL insert_test_data();

-- 锁升级死锁(通常发生在没有合适索引时)
-- 事务1
START TRANSACTION;
-- 没有合适的索引,可能升级为表锁
UPDATE large_table SET status = 1 WHERE data LIKE 'Data-1%';
-- 更新了大约1000行,可能持有大量行锁

-- 事务2
START TRANSACTION;
-- 同样更新大量行
UPDATE large_table SET status = 2 WHERE data LIKE 'Data-2%';

-- 事务1
-- 尝试访问事务2锁定的行
UPDATE large_table SET status = 3 WHERE id = 2001;

-- 事务2
-- 尝试访问事务1锁定的行
UPDATE large_table SET status = 3 WHERE id = 1001;

-- 结果:锁升级+资源竞争 → 死锁

诊断和排查死锁信息

一、查看死锁信息

perl 复制代码
-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS\G;

-- 重点查看 LATEST DETECTED DEADLOCK 部分
/*
LATEST DETECTED DEADLOCK
------------------------
2024-01-07 10:00:00 0x7f8e2c0e6700
*** (1) TRANSACTION:
TRANSACTION 123456, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 100, OS thread handle 12345, query id 1000 localhost root updating
UPDATE users SET balance = balance - 100 WHERE id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 3 n bits 72 index PRIMARY of table `test`.`users` 
trx id 123456 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; ...

*** (2) TRANSACTION:
TRANSACTION 123457, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 12346, query id 1001 localhost root updating
UPDATE users SET balance = balance - 200 WHERE id = 1
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 3 n bits 72 index PRIMARY of table `test`.`users` 
trx id 123457 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; ...

*** WE ROLL BACK TRANSACTION (2)
*/

二、启用死锁日志

ini 复制代码
-- 临时开启死锁日志
SET GLOBAL innodb_print_all_deadlocks = ON;

-- 查看错误日志中的死锁信息
-- Linux: /var/log/mysql/error.log
-- 查看最后100行
SHOW VARIABLES LIKE 'log_error';
-- 然后查看对应的日志文件

-- 配置my.cnf永久启用
[mysqld]
innodb_print_all_deadlocks = 1
innodb_status_output = 1
innodb_status_output_locks = 1

三、监控锁信息

sql 复制代码
-- 查看当前锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;

-- 综合查询:查看事务和锁等待
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b 
    ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r 
    ON r.trx_id = w.requesting_trx_id;

使用Performance Schema监控

sql 复制代码
-- 启用锁监控
UPDATE performance_schema.setup_consumers 
SET ENABLED = 'YES' 
WHERE NAME LIKE 'events_transactions%' 
   OR NAME LIKE 'events_statements%' 
   OR NAME LIKE 'events_stages%';

-- 查看锁事件
SELECT * FROM performance_schema.events_waits_current 
WHERE EVENT_NAME LIKE '%lock%';

-- 查看死锁历史
SELECT 
    THREAD_ID,
    EVENT_NAME,
    SOURCE,
    TIMER_WAIT/1000000000 as wait_seconds,
    LOCK_TYPE,
    LOCK_DURATION,
    LOCK_STATUS
FROM performance_schema.events_waits_history
WHERE EVENT_NAME LIKE '%wait/lock%'
ORDER BY TIMER_WAIT DESC
LIMIT 10;
相关推荐
小熊officer8 分钟前
Python字符串
开发语言·数据库·python
渐暖°21 分钟前
JDBC直连ORACLE进行查询
数据库·oracle
萧曵 丶1 小时前
Next-Key Lock、记录锁、间隙锁浅谈
数据库·sql·mysql·mvcc·可重复读·幻读
做cv的小昊1 小时前
【TJU】信息检索与分析课程笔记和练习(7)数据库检索—Ei
数据库·笔记·学习·全文检索
zgl_200537792 小时前
ZGLanguage 解析SQL数据血缘 之 标识提取SQL语句中的目标表
java·大数据·数据库·数据仓库·hadoop·sql·源代码管理
莳花微语2 小时前
记录一次OGG进程abended,报错OGG-01431、OGG-01003、OGG-01151、OGG-01296问题的处理
数据库·sql·mysql
尋有緣2 小时前
力扣1355-活动参与者
大数据·数据库·leetcode·oracle·数据库开发
萧曵 丶2 小时前
MySQL三大日志系统浅谈
数据库·sql·mysql
煎蛋学姐2 小时前
SSM校园兼职招聘系统x6u36(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·企业管理·ssm 框架·校园兼职招聘系统
ChineHe4 小时前
Redis基础篇004_Redis Pipeline流水线详解
数据库·redis·缓存