即使您在单个事务中执行 SELECT 后跟 UPDATE,您仍然有可能出现竞争条件。SELECT 本身不会锁定任何内容,因此您可以有两个并发会话 SELECT 并获得相同的 id。然后两者都会尝试更新,但只有一个会“获胜”——另一个必须等待。
要解决此问题,请使用 SELECT...FOR UPDATE 子句,该子句在它返回的行上创建一个锁。
Prepare Transaction...
$id = SELECT id
FROM companies
WHERE processing = 0
ORDER BY last_crawled ASC
LIMIT 1
FOR UPDATE;
这意味着锁定是在选择行时创建的。这是原子的,这意味着没有其他会话可以潜入并获得同一行的锁定。如果他们尝试,他们的事务将在 SELECT 上阻塞。
UPDATE companies
SET processing = 1
WHERE id = $id;
Commit Transaction
我将您的“执行交易”伪代码更改为“提交交易”。事务中的语句会立即执行,这意味着它们会创建锁等等。然后,当您提交时,锁将被释放并提交任何更改。已提交意味着它们不能回滚,并且它们对其他事务可见。
这是使用 mysqli 完成此操作的快速示例:
$mysqli = new mysqli(...);
$mysqli->report_mode = MYSQLI_REPORT_STRICT; /* throw exception on error */
$mysqli->begin_transaction();
$sql = "SELECT id
FROM companies
WHERE processing = 0
ORDER BY last_crawled ASC
LIMIT 1
FOR UPDATE";
$result = $mysqli->query($sql);
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
$id = $row["id"];
}
$sql = "UPDATE companies
SET processing = 1
WHERE id = ?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("i", $id);
$stmt->execute();
$mysqli->commit();
回复您的评论:
我尝试了一个实验并创建了一个表companies
,用 512 行填充它,然后启动一个事务并发出SELECT...FOR UPDATE
上面的语句。我是在mysql客户端做的,不需要写PHP代码。
然后,在提交我的事务之前,我检查了报告的锁:
mysql> show engine innodb status\G
=====================================
2013-12-04 16:01:28 7f6a00117700 INNODB MONITOR OUTPUT
=====================================
...
---TRANSACTION 30012, ACTIVE 2 sec
2 lock struct(s), heap size 376, 513 row lock(s)
...
尽管使用LIMIT 1
了 ,但此报告显示事务似乎锁定了表中的每一行(由于某种原因,加 1)。
所以你是对的,如果你每秒有数百个请求,那么事务很可能正在排队。您应该能够通过观察SHOW PROCESSLIST
和看到许多进程陷入某种状态Locked
(即等待访问另一个线程已锁定的行)来验证这一点。
如果您每秒有数百个请求,您可能已经超出了 RDBMS 充当假消息队列的能力。这不是 RDBMS 擅长的。
有多种与 PHP 良好集成的可扩展消息队列框架,如 RabbitMQ、STOMP、AMQP、Gearman、Beanstalk。
查看http://www.slideshare.net/mwillbanks/message-queues-a-primer-international-php-conference-fall-2012