我正在尝试建立一个先到先得的模型销售页面。我们有 n 个相同类型的项目。我们希望将这 n 个项目分配给提出请求的前 n 个用户。每个项目对应一个数据库行。当用户按下购买按钮时,系统会尝试查找尚未售出的条目( reservationCompleted = FALSE
)并更新用户 ID 并设置 reservationCompleted
为真。
由于我使用的数据库引擎是 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 条件吗?
最佳答案
在比赛情况下将遵守 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
的角色在你的 table 上,updated_by_connection_id
就像 assignedPhone
, 和 locked
喜欢 reservationCompleted
.现在让我们开始比赛测试。您应该打开 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;
Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 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
Query OK, 0 rows affected (23.25 sec) Rows matched: 0 Changed: 0 Warnings: 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;
Empty set (0.00 sec)
连接 2
start transaction;
SELECT * FROM table2;
Empty set (0.00 sec)
连接 1
UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 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释放继续,输出如下:
Query OK, 0 rows affected (20.47 sec) Rows matched: 0 Changed: 0 Warnings: 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;
之后,创建快照。所有 future 的非更新读取都将从该快照完成。因此,如果您只是在更新后天真地 SELECT,它将从原始快照中提取信息,即 之前 该行已更新。通过执行 SELECT ... FOR UPDATE 你强制它获得正确的信息。
然而,在实际应用中,这可能是一个问题。比如说,你的请求被包装在一个事务中,并且在执行更新之后你想要输出一些信息。收集和输出该信息可能由单独的、可重用的代码处理,您不想“以防万一”在 FOR UPDATE 子句中乱扔垃圾。由于不必要的锁定,这会导致很多挫折。
相反,你会想要走一条不同的道路。你在这里有很多选择。
一是确保在 UPDATE 完成后提交事务。在大多数情况下,这可能是最好、最简单的选择。
另一种选择是不要尝试使用 SELECT 来确定结果。相反,您可以读取受影响的行,并使用它(更新 1 行 vs 更新 0 行)来确定 UPDATE 是否成功。
另一种选择,也是我经常使用的选择,因为我喜欢将单个请求(如 HTTP 请求)完全包装在单个事务中,是确保事务中执行的第一条语句是 UPDATE 或 SELECT ... FOR UPDATE .这将导致在允许连接继续之前不拍摄快照。
让我们再次重置我们的测试数据库,看看它是如何工作的。
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;
Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 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;
Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 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 中,SELECT 和 SELECT ... FOR UPDATE 给出了相同的结果。这是因为 SELECT 从中读取的快照是在提交连接 1 之后才创建的。
因此,回到您最初的问题:是的,在所有情况下,UPDATE 语句都会检查 WHERE 子句。但是,您必须小心处理您可能正在执行的任何 SELECT,以避免错误地确定该 UPDATE 的结果。
(是的,另一种选择是更改事务隔离级别。但是,我真的没有这方面的经验以及可能存在的任何问题,所以我不打算深入研究。)
关于php - MySQL 更新查询 - 竞争条件和行锁定会遵守 'where' 条件吗? (php, PDO, MySQL, InnoDB),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48616993/