繁体   English   中英

MySQL 更新查询 - 竞争条件和行锁定会遵守“where”条件吗? (php, PDO, MySQL, InnoDB)

[英]MySQL Update query - Will the 'where' condition respected on race condition and row locking? (php, PDO, MySQL, InnoDB)

我正在尝试建立一个先到先得的模型销售页面。 我们有 n 个相同类型的项目。 我们希望将这 n 个项目分配给提出请求的前 n 个用户。 每个项目对应一个数据库行。 当用户按下购买按钮时,系统会尝试查找尚未售出的条目( reservationCompleted = FALSE )并更新用户 ID 并将reservationCompleted设置为true。

由于我使用的数据库引擎是 InnoDB,我知道有一个内部锁定机制,不允许两个进程在同一行上同时进行更新。

我的问题是,

如果我使用的语句如下,如果两个请求同时到达,这是否会导致不同的用户被分配到同一行?

$query = "UPDATE available_items
    SET assignedPhone=".$user->phone.",
        reservationCompleted = TRUE,
        assignmentCreatedTimestamp =".time()."
    WHERE id=".$itemListing['id']."
    AND reservationCompleted=FALSE";
$stmt = $pdo->prepare($query);
$stmt->execute();

考虑以下情况。

两个不同的进程获取同一行(比如 id=5)并尝试更新数据库条目。 但是其中一个得到了锁。 它更新项目并释放锁,下一个进程获得锁。 那么,它会在执行更新之前再次验证 where 条件吗?

在比赛情况下,哪里的条件会受到尊重,但你必须小心检查谁是谁赢得了比赛。

请考虑以下演示如何工作以及为什么必须小心。

首先,设置一些最小的表。

CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id扮演的角色id在你的餐桌, updated_by_connection_id就像assignedPhone ,并lockedreservationCompleted

现在让我们开始比赛测试。 您应该打开2个命令行/终端窗口,连接到mysql并使用创建这些表的数据库。

连接1

start transaction;

连接2

start transaction;

连接1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

查询正常,1行受影响(0.00秒)匹配的行数:1已更改:1警告:0

连接2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

连接2正在等待

连接1

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
commit;

此时,连接2被释放以继续并输出以下内容:

连接2

查询正常,0行受影响(23.25秒)匹配的行数:0已更改:0警告:0

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
commit;

一切都很好看。 我们看到是的,WHERE子句在竞争情况下得到尊重。

我说你必须小心的原因是因为在一个真实的应用程序中,事情并不总是这么简单。 您可以在事务中进行其他操作,这实际上可以更改结果。

让我们用以下内容重置数据库:

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

现在,考虑这种情况,其中在UPDATE之前执行SELECT。

连接1

start transaction;

SELECT * FROM table2;

空集(0.00秒)

连接2

start transaction;

SELECT * FROM table2;

空集(0.00秒)

连接1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

查询正常,1行受影响(0.00秒)匹配的行数:1已更改:1警告:0

连接2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

连接2正在等待

连接1

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 1 row in set (0.00 sec) 
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 1 row in set (0.00 sec) 
commit;

此时,连接2被释放以继续并输出以下内容:

查询正常,0行受影响(20.47秒)匹配的行数:0已更改:0警告:0

好的,让我们看看谁赢了:

连接2

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 0 | NULL | +----+--------+--------------------------+ 

等等,什么? 为什么locked 0并且updated_by_connection_id空?

这是我提到的小心。 罪魁祸首实际上是因为我们在开始时做了一个选择。 为了获得正确的结果,我们可以运行以下命令:

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
commit;

通过使用SELECT ... FOR UPDATE,我们可以得到正确的结果。 这可能非常令人困惑(因为对我而言,最初),因为SELECT和SELECT ... FOR UPDATE给出了两个不同的结果。

发生这种情况的原因是由于默认隔离级别READ-REPEATABLE 当第一个SELECT完成后,就在start transaction; ,创建快照。 所有未来的非更新读取都将从该快照完成。

因此,如果您在执行更新后只是天真地选择SELECT,它将从该原始快照中提取信息,该快照在更新行之前 通过执行SELECT ... FOR UPDATE,您可以强制它获取正确的信息。

然而,再次,在实际应用中,这可能是一个问题。 例如,假设您的请求包含在事务中,并且在执行更新后您想要输出一些信息。 收集和输出该信息可以由单独的,可重复使用的代码处理,您不希望使用FOR UPDATE子句“以防万一”。 由于不必要的锁定,这会导致很多挫败感。

相反,你会想要采取不同的轨道。 你有很多选择。

一,是确保在UPDATE完成后提交事务。 在大多数情况下,这可能是最好,最简单的选择。

另一种选择是不要尝试使用SELECT来确定结果。 相反,您可以读取受影响的行,并使用它(更新1行,更新0行)以确定UPDATE是否成功。

另一个选项,也就是我经常使用的选项,因为我希望将单个请求(如HTTP请求)完全包装在单个事务中,这是为了确保在事务中执行的第一个语句是UPDATE或SELECT ...更新 这将导致在允许连接继续之前不会拍摄快照。

让我们再次重置我们的测试数据库,看看它是如何工作的。

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

连接1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 0 | NULL | +----+--------+--------------------------+ 

连接2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

连接2正在等待。

连接1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

查询正常,1行受影响(0.01秒)匹配的行数:1已更改:1警告:0

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
commit;

连接2现已发布。

连接2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

在这里,您实际上可以让您的服务器端代码检查此SELECT的结果并知道它是准确的,甚至不再继续下一步。 但是,为了完整起见,我会像以前一样完成。

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

查询正常,0行受影响(0.00秒)匹配的行数:0已更改:0警告:0

SELECT * FROM table1 WHERE id = 1;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
 +----+--------+--------------------------+ | id | locked | updated_by_connection_id | +----+--------+--------------------------+ | 1 | 1 | 1 | +----+--------+--------------------------+ 
commit;

现在您可以看到在Connection 2中SELECT和SELECT ... FOR UPDATE给出相同的结果。 这是因为SELECT提取的快照直到提交连接1之后才创建。

因此,回到原来的问题:是的,在所有情况下,UPDATE语句都会检查WHERE子句。 但是,您必须小心您可能正在执行的任何SELECT,以避免错误地确定该UPDATE的结果。

(是的另一种选择是改变事务隔离级别。但是,我并没有真正的经验,可能存在任何可能存在的问题,所以我不打算进入它。)

答案是: 它将在更新数据之前验证WHERE条件


好吧,我不得不说这是一个非常有趣的问题。 我以前从未想过这样的问题,它强迫我更好地理解它在MySQL中是如何工作的。 谢谢!

我是如何得到答案的:

我首先考虑了这种情况。 我知道甚至在我做测试之前它应该像这样工作,但我只是不明白为什么。

为什么:

最后,我在Index Condition Pushdown部分找到了一些有用的东西。

这是它在MySQL中的工作原理:

MySQL Server
   ↑  ↑
   ↓  ↓
Storage Engine(InnoDB here)
  1. MySQL Server解析来自外部应用程序的SQL语句。
  2. MySQL Server告诉InnoDB为它检索行。
  3. InnoDB定位行(索引,锁定),然后将它们返回到MySQL服务器。
  4. MySQL服务器评估行的WHERE条件。
  5. 其他一些事情......

如您所见,InnoDB内部发生锁定,MySQL Server在获取行后评估WHERE条件。 对于您的情况,行(id = 5)被第一个UPDATE锁定,第二个UPDATE在获取同一行时被卡住。 并且在获得行的锁定之后,对第二个UPDATEWHERE条件进行评估。

更重要的是,如果您在id上创建了索引,则会在您的查询中进行索引条件下推。

不,因为reservationCompleted设置为true true 。不要忘记COMMIT每个成功的事务。 下一个进程当然会获得锁定,但不会满足WHERE条件并释放LOCK。如果您希望下一个进程查找另一个可用项目您可以使用Sub Routine过程包装Update语句来检查是否reservationCompletedFALSE

因为您正在使用PDO,所以应该考虑在语句中使用参数 此外,您在数据库中的时间戳应存储为DATETIME,以便您可以在查找中利用MySQL的搜索功能。

<?php
$sql = "UPDATE available_items SET assignedPhone=:userPhone,
 reservationCompleted = TRUE,
 assignmentCreatedTimestamp =:time
 WHERE id=:id AND reservationCompleted=FALSE";
$stmt = $db->prepare($sql);
$stmt->bindValue(':userPhone', $user->phone, PDO::PARAM_STR);
$stmt->bindValue(':time', time(), PDO::PARAM_INT);
$stmt->bindValue(':id', $itemListing['id'], PDO::PARAM_INT);
$stmt->execute();
$affected_rows = $stmt->rowCount();

总之执行你谈论你需要每次交易之前,如果你希望它是如此接近在一起时做一个查询的行动,否则你可能会疏远你的目标受众。 您还需要利用异步连接(想想AJAX )在潜在事务期间不断检查项目的状态,并可能提供消息传递服务,以便在无法进行事务时让某人知道。 除非您的数据库与Web服务器之间的连接速度较慢,否则数据库锁定的持续时间不足以发生此情况。

你可能也想研究类似SOAP的东西。 然后,您将在系统上编写一个表示每个项目状态的XML文件或JSON文件,并且您的客户端代码只会检查它是否有更新,而不是在页面打开时不断执行异步MySQL查询。 成功执行事务后,还会更新此文件和数据库。 这样您只需要涉及Web服务器而不是Web服务器和数据库服务器。

如果你想要的话,你也可以考虑使用microtime()来降低微秒。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM