MySQL:面对繁重的并发性时的正确性(SELECT ... FOR UPDATE)

时间:2009-09-26 08:42:53

标签: mysql concurrency

我正在运行一个用户可以采用虚拟宠物的网站。每只宠物都有每用户采用限制。因此,例如,您可以最多使用我们的一只宠物10次。目前我们做这样的事情:

CREATE TABLE `num_adopted` (
  `petid` int(11) NOT NULL,
  `userid` int(11) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  PRIMARY KEY (`petid`,`userid`),
) ENGINE=InnoDB

然后当有人收养宠物时我们:

START TRANSACTION;

SELECT total
FROM num_adopted
WHERE petid=? AND userid=?
FOR UPDATE

我们会根据limit检查总数。如果他们已经采用limit,我们ROLLBACK并告诉用户。否则我们:

INSERT INTO num_adopted (petid, userid, total)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE total=total+1

然后我们在另一个表中添加一行来记录他们的新宠物,最后:

COMMIT

这必须在非常高的并发性下完美运行。

现在,如果限制为10且用户已采用 limit-1 pet,我可以看到第一个FOR UPDATE中的SELECT注释保证总计将被锁定,以便多个并发采用不会最终看到总计为limit-1(这将允许用户超过他们的limit)。第一次采用将看到total = limit-1并成功完成,其余将阻止。最终,其他人将看到总数=限制并且拒绝接受另一只宠物。

但如果limit = 1且total = 0怎么办?多个采用事务可以同时在num_adopted表中看不到行(so total = 0),允许用户采用多个宠物吗?目前尚不清楚FOR UPDATE是否可以锁定不存在的行。如果是这样,这个改变的方案会解决问题吗?

START TRANSACTION;

INSERT INTO num_adopted (petid, userid, total)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE total=total+1;

SELECT total
FROM num_adopted
WHERE petid=? AND userid=?

检查是否总计>限制,如果是,ROLLBACK。否则记录采用的宠物和COMMIT

1 个答案:

答案 0 :(得分:1)

我通过在检查当前总数和更新它之间添加一个大睡眠()来测试两种方法并模拟高并发性。我描述的第一种方法是这样检查的:

<?php

$limit=1;

mysql_query("START TRANSACTION") or die("!");

echo "Checking the user's adoption count...\n";

$result=mysql_query("
    SELECT total
    FROM num_adopted
    WHERE petid=1 AND userid=1
    FOR UPDATE") or die(mysql_error());

$row = mysql_fetch_row($result);

$count = $row ? $row[0] : 0;

echo "Adoption count before adoption is: $count\n";

if ($count>=$limit) {
    echo "Limit reached, not allowing adoption\n";
    mysql_query("ROLLBACK");
    exit;
}

echo "Wait for 5 seconds...\n";
sleep(5);
echo "Recording the new adoption...\n";

mysql_query("
    INSERT INTO num_adopted (petid, userid, total)
    VALUES (1, 1, 1)
    ON DUPLICATE KEY UPDATE total=total+1") or die(mysql_error());

mysql_query("COMMIT") or die(mysql_error());

echo "Pet has been adopted.\n";

?>

如果我开始使用空数据库,它会检查总数(并发现它等于零),然后它会休眠5秒钟。在它睡觉的时候,我开始另一次收养。第二次采用检查总数(也发现它等于零)并休眠。第一次采用阻止其INSERT等待第二次采用以释放其锁定。第二个采用阻止其INSERT等待第一次采用以释放其锁定。幸运的是,它注意到它与第一个事务处于死锁状态,并以错误消息中止。所以SELECT ... FOR UPDATE方法确实提供了我需要的安全性。但是不要因为死锁而中止。采用第二种方法:

<?php

$limit=1;

mysql_query("START TRANSACTION") or die("!");

echo "Updating the user's adoption count...\n";

mysql_query("
    INSERT INTO num_adopted (petid, userid, total)
    VALUES (1, 1, 1)
    ON DUPLICATE KEY UPDATE total=total+1") or die(mysql_error());

echo "Checking the user's adoption count...\n";

$result=mysql_query("
    SELECT total
    FROM num_adopted
    WHERE petid=1 AND userid=1") or die(mysql_error());

$row=mysql_fetch_row($result);

if (!$row) die("Something's hinky...");

$count=$row[0];

echo "Adoption count after adoption will be: $count\n";

if ($count>$limit) {
    echo "Limit reached, not allowing adoption\n";
    mysql_query("ROLLBACK");
    exit;
}

echo "Wait for 5 seconds...\n";
sleep(5);
echo "Recording the new adoption...\n";

mysql_query("COMMIT") or die(mysql_error());

echo "Pet has been adopted.\n";

?>

第二次采用阻止它在第一次采用完成之前进行INSERT INTO。第一个事务完成后,第二个事务可以继续。它正确检测到已达到限制,而无需提供有关死锁的错误消息。