我经常使用一些似乎死锁的代码。在Java中,它会定期生成DeadLockLoserDataAccessException
,导致死锁的违规语句通常是本身 。 (这是在与InnoDB的交易中运行的)
UPDATE a
SET
a_field = (SELECT sum(b_field) FROM b WHERE b.a_id = a.id)
WHERE
a = ?
在做了一些阅读之后,我遇到了执行锁定读取的FOR UPDATE
子句。所以我修改了下面的代码
UPDATE a
SET
a_field = (SELECT sum(b_field) FROM b WHERE b.a_id = a.id FOR UPDATE)
WHERE
a = ?
在嵌套FOR UPDATE
中添加UPDATE/SELECT
锁是否合适? Locking Reads Documentation上的所有示例都没有以这种方式使用FOR UPDATE
。
以下是简化版本,其字段仅适用于查询
id int(11) PRIMARY KEY
a_field int(11)
id int(11) PRIMARY KEY
a_id int(11) FOREIGN KEY REFERENCES (a.id)
b_field int(11)
唯一存在的索引是两个主键上的单列索引,以及表a的外键。
答案 0 :(得分:3)
您问题的一个简单答案是:
是的,MySql支持子查询中的
FOR UPDATE
子句
Hovewer肯定不能解决您的问题
子查询中的FOR UPDATE在这种情况下不会阻止死锁。
由于您没有向我们展示整个事务,而只是一个片段,我的猜测是在事务中必须有一些其他命令对外键引用的记录进行锁定。
为了更好地理解MySql中锁定的工作原理,请看一下这个简单的例子:
CREATE TABLE `a` (
`id` int(11) primary key AUTO_INCREMENT,
`a_field` int(11)
);
CREATE TABLE `b` (
`id` int(11) primary key AUTO_INCREMENT,
`a_id` int(11),
`b_field` int(11),
CONSTRAINT `b_fk_aid` FOREIGN KEY (`a_id`) REFERENCES `a` (`id`)
);
CREATE TABLE `c` (
`id` int(11) primary key AUTO_INCREMENT,
`a_id` int(11),
`c_field` int(11),
CONSTRAINT `c_fk_aid` FOREIGN KEY (`a_id`) REFERENCES `a` (`id`)
);
insert into a( a_field ) values ( 10 ), ( 20 );
insert into b( a_id, b_field ) values ( 1, 20 ), ( 2, 30 );
delimiter $$
create procedure test( p_a_id int, p_count int )
begin
declare i int;
set i = 0;
REPEAT
START TRANSACTION;
INSERT INTO c( a_id, c_field ) values ( p_a_id, round(rand() * 100) );
UPDATE a
SET a_field = (
SELECT sum(b_field)
FROM b WHERE b.a_id = a.id
FOR UPDATE)
WHERE
id = p_a_id;
commit;
set i = i + 1;
until i > p_count
end repeat;
end $$
DELIMITER ;
请注意,子查询中使用了FOR UPDATE
如果我们同时在两个会话中执行该过程:
call test( 2, 400 );
我们几乎立即得到了一个死锁错误:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2013-09-05 23:08:27 1b8c
*** (1) TRANSACTION:
TRANSACTION 1388056, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 6, OS thread handle 0x1db0, query id 3107246 localhost 127.0.0.1 test updating
UPDATE a
SET a_field = (
SELECT sum(b_field)
FROM b WHERE b.a_id = a.id
FOR UPDATE)
WHERE
id = p_a_id
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388056 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000000152e16; asc . ;;
2: len 7; hex 2d0000013b285a; asc - ;(Z;;
3: len 4; hex 8000001e; asc ;;
*** (2) TRANSACTION:
TRANSACTION 1388057, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 2, locked 2
5 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 7, OS thread handle 0x1b8c, query id 3107247 localhost 127.0.0.1 test updating
UPDATE a
SET a_field = (
SELECT sum(b_field)
FROM b WHERE b.a_id = a.id
FOR UPDATE)
WHERE
id = p_a_id
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388057 lock mode S locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000000152e16; asc . ;;
2: len 7; hex 2d0000013b285a; asc - ;(Z;;
3: len 4; hex 8000001e; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388057 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000000152e16; asc . ;;
2: len 7; hex 2d0000013b285a; asc - ;(Z;;
3: len 4; hex 8000001e; asc ;;
*** WE ROLL BACK TRANSACTION (2)
------------
如您所见,MySql报告死锁错误是由相同的两个UPDAT引起的。
然而,这只是事实的一半。
死锁错误的真正原因是INSERT INTO c
语句,它在A表中的引用记录上放置了共享锁(由于C
表中的FOREIGN KEY约束)。
并且 - 令人惊讶的是 - 为了防止死锁,必须在事务开始时对A
表中的行进行锁定:
declare dummy int;
......
START TRANSACTION;
SELECT id INTO dummy FROM A
WHERE id = p_a_id FOR UPDATE;
INSERT INTO c( a_id, c_field ) values ( p_a_id, round(rand() * 100) );
UPDATE a
SET a_field = (
SELECT sum(b_field)
FROM b WHERE b.a_id = a.id
)
WHERE
id = p_a_id;
commit;
此更改后,程序运行时没有死锁。
因此,您可以尝试在交易开始时添加SELECT ... FROM A ... FOR UPDATE
。
但如果这不起作用,请进一步帮助解决这个问题,请:
答案 1 :(得分:1)
如果单个查询陷入死锁,则必须是MySQL错误。单独的事务必须永远不会陷入死锁。 检查单元测试并进入MySQL bug database。
更新行时,某些RDBMS会锁定该行以防止复杂/错误的合并算法。可能是你的代码正在运行许多事务,而且它们都有死锁?
如果错误得到证实,您可以拆分查询(这看起来非常简单):
SELECT id FROM a WHERE id=? FOR UPDATE;
SELECT SUM(b_field) FROM b WHERE b.a_id=?;
UPDATE a SET a_field=? WHERE id=?;
COMMIT
PS:我认为a = ?
表示a.id = ?
?