死锁的本质
两个事务同时操作同样的数据,然后互相等待对方释放锁。
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;