【问题标题】:Are queries inside a MySQL transaction guaranteed to be executed together?MySQL 事务中的查询是否保证一起执行?
【发布时间】:2013-11-12 23:09:55
【问题描述】:

我有一个进程根据一些标准从 MySQL InnoDB 表中选择下一个要处理的项目。当已选择一行作为旁边的进程时,它的processing字段设置为1,同时处理正在进行数据库外。我这样做是为了可以同时运行多个处理器,并且它们不会处理同一行。

如果我使用事务来执行以下查询,它们是否保证一起执行(例如,没有任何其他 MySQL 连接执行查询。)?如果不是,那么多个处理器可能会从 SELECT 查询中获得相同的 id,然后处理将是多余的。


伪代码示例

Prepare Transaction...

$id = SELECT id
FROM companies
WHERE processing = 0
ORDER BY last_crawled ASC
LIMIT 1;

UPDATE companies
SET processing = 1
WHERE id = $id;

Execute Transaction

我一直在努力使用单个 UPDATE 查询 (see this question) 以足够快的速度完成此任务。假设这不是本问题的一个选项。

【问题讨论】:

    标签: php mysql transactions mysqli


    【解决方案1】:

    即使您在单个事务中执行 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

    【讨论】:

    • 如何将 $id 传递给事务中的第二个查询,然后在事务提交后如何检索它?我在 PHP 中使用mysqli。对我的懒惰和使用伪代码的监督。 :)
    • 我对这个答案投了反对票,但如果反对者能解释为什么他们认为它不够好,我很乐意改进它。
    • 此方法是否只锁定SELECT ID 查询中的单行?当我有数百个进程同时发出这个请求时,我的行为很奇怪。这个查询变得非常耗费资源,我无法确定原因。查询时间从 100 毫秒跳到很多秒,在我看来这是并行进程相互“绊倒”的结果。
    【解决方案2】:

    这取决于。 SQL 中有(通常)不同的隔离级别。在 MySQL 中,您可以使用 SET TRANSACTION ISOLATION LEVEL 更改使用哪一个。

    虽然“SERIALIZABLE”(这是最严格的)仍然不意味着在您的交易中没有执行其他操作,但它确实可以确保同时执行的交易没有区别与否 - 如果它会有所作为,事务将回滚并稍后执行。

    但是请注意,隔离越严格,需要执行的锁定和回滚就越多。所以在使用它之前确保你真的需要它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-19
      • 1970-01-01
      • 1970-01-01
      • 2019-11-17
      • 2016-06-01
      • 2012-05-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多