考虑以下perl代码:
$schema->txn_begin();
my $r = $schema->resultset('test1')->find({id=>20});
my $n = $r->num;
$r->num($n+1);
print("updating for $$\n");
$r->update();
print("$$ val: ".$r->num."\n");
sleep(4);
$schema->txn_commit();
我期待由于更新受到事务的保护,因此如果两个进程尝试更新“num”字段,则第二个应该失败并出现一些错误,因为它丢失了竞争。 Interbase将此称为“死锁”错误。然而,MySQL会在update()调用上暂停,但在第一个调用commit之后会很乐意继续。然后第二个进程具有num的“old”值,导致增量不正确。观察:
$ perl trans.pl & sleep 1 ; perl trans.pl
[1] 5569
updating for 5569
5569 val: 1015
updating for 5571
5571 val: 1015
[1]+ Done perl trans.pl
两种情况下的结果值均为“1015”。这怎么可能是正确的?
答案 0 :(得分:5)
假设您使用InnoDB作为存储引擎,这就是我期望的行为。 InnoDB的默认事务隔离级别为REPEATABLE READ
。这意味着当您执行SELECT
时,事务将在该特定时间获得数据库的snapshot。快照将不包含尚未提交的其他事务的更新数据。由于每个进程中的SELECT
在提交之前发生,因此它们每个都会看到数据库处于相同的状态(num = 1014)。
为了获得您似乎期待的行为,您应该遵循Lluis的建议并执行SELECT ... FOR UPDATE
来锁定您感兴趣的行。为此,请更改此行
my $r = $schema->resultset('test1')->find({id=>20});
到这个
my $r = $schema->resultset('test1')->find({id=>20}, {for=>'update'});
并重新运行测试。
如果您不熟悉MySQL中交易的复杂性,我强烈建议您阅读有关InnoDB Transaction Model and Locking的文档中的部分。另外,如果您还没有,请仔细阅读有关交易的DBIC Usage Notes和AutoCommit
。当txn_
打开或关闭时AutoCommit
方法的行为方式有点棘手。如果你愿意的话,我也建议阅读消息来源。就个人而言,我必须阅读源代码才能完全理解DBIC正在做什么,以便我能够得到我想要的确切行为。
答案 1 :(得分:0)
尝试将$ r-> num存储在mysql变量而不是perl中。 对不起,我不知道Perl,但基本上你想要的是
START TRANSACTION;
SELECT num INTO @a FROM test1 where id = 20;
UPDATE test1 set num=(@a+1) WJERE id=20;
COMMIT;
答案 2 :(得分:0)
这不是死锁,死锁是这样的:
Tx1
1-更新R1 =>在R1上写锁 2-更新R2 =>在R2上写锁定
Tx 2
1-更新R2 2-更新R1
如果tx1和tx2同时执行,则可能发生tx1等待R2上的锁定空闲,并且tx2等待R1上的锁定。
在您的情况下,您需要锁定id = 20的行(使用select for update)。到达“迟到”的tx将等待一段时间(由db引擎定义)。