在比赛中,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
一样,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;
查询成功,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
又是NULL?
这就是我提到的小心谨慎。罪魁祸首实际上是因为我们在开头做了一个选择。为了得到正确的结果,我们可以运行以下命令:
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行更新 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 |
+
Connection 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;
现在你可以看到,在连接2中SELECT和SELECT ... FOR UPDATE的结果是相同的。这是因为SELECT从快照中读取,而该快照直到Connection 1提交后才创建。
所以,回到你最初的问题:是的,在所有情况下,WHERE子句都会被UPDATE语句检查。但是,你必须小心任何可能进行的SELECT操作,以避免错误地确定该UPDATE的结果。
(是的,另一个选择是更改事务隔离级别。然而,我对此没有太多经验,也不知道可能存在的任何陷阱,因此我不打算深入探讨。)
UPDATE
是否在自动提交模式下(非事务中)运行? - Rick James