我正在运行一个用户可以采用虚拟宠物的网站。每只宠物都有每用户采用限制。因此,例如,您可以最多使用我们的一只宠物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
。
答案 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。第一个事务完成后,第二个事务可以继续。它正确检测到已达到限制,而无需提供有关死锁的错误消息。