MySQL Update查询 - 在竞争条件和行锁定时是否会遵守"where"条件?(php,PDO,MySQL,InnoDB)

Akh*_*nan 7 php mysql pdo 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();
Run Code Online (Sandbox Code Playgroud)

考虑以下情况.

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

Wil*_*ema 5

在比赛情况下将遵守where条件,但是您必须小心如何检查谁赢得了比赛。

Consider the following demonstration of how this works and why you have to be careful.

First, setup some minimal tables.

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);
Run Code Online (Sandbox Code Playgroud)

id plays the role of id in your table, updated_by_connection_id acts like assignedPhone, and locked like reservationCompleted.

Now lets start the race test. You should have 2 commandline/terminal windows open, connected to mysql and using the database where you have created these tables.

Connection 1

start transaction;
Run Code Online (Sandbox Code Playgroud)

Connection 2

start transaction;
Run Code Online (Sandbox Code Playgroud)

Connection 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

Connection 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Connection 2 is now waiting

Connection 1

SELECT * FROM table1 WHERE id = 1;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
commit;
Run Code Online (Sandbox Code Playgroud)

At this point, connection 2 is released to continue and outputs the following:

Connection 2

Query OK, 0 rows affected (23.25 sec) Rows matched: 0 Changed: 0 Warnings: 0

SELECT * FROM table1 WHERE id = 1;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
commit;
Run Code Online (Sandbox Code Playgroud)

Everything looks fine. We see that yes, the WHERE clause was respected in a race situation.

The reason I said you had to be careful though, is because in a real application things are not always this simple. You MAY have other actions going on within the transaction, and that can actually change the results.

Let's reset the database with the following:

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);
Run Code Online (Sandbox Code Playgroud)

And now, consider this situation, where a SELECT is performed before the UPDATE.

Connection 1

start transaction;

SELECT * FROM table2;
Run Code Online (Sandbox Code Playgroud)

Empty set (0.00 sec)

Connection 2

start transaction;

SELECT * FROM table2;
Run Code Online (Sandbox Code Playgroud)

Empty set (0.00 sec)

Connection 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

Connection 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Connection 2 is now waiting

Connection 1

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

At this point, connection 2 is released to continue and outputs the following:

Query OK, 0 rows affected (20.47 sec) Rows matched: 0 Changed: 0 Warnings: 0

Ok, let's see who won:

Connection 2

SELECT * FROM table1 WHERE id = 1;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      0 |                     NULL |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

Wait, what? Why is locked 0 and updated_by_connection_id NULL??

This is the being careful I mentioned. The culprit is actually due to the fact that we did a select at the beginning. To get the correct result, we could run the following:

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
commit;
Run Code Online (Sandbox Code Playgroud)

By using SELECT ... FOR UPDATE we can get the right result. This can be very confusing (as it was for me, originally), as a SELECT and a SELECT ... FOR UPDATE are giving two different results.

The reason this happens is because of the default isolation level READ-REPEATABLE. When the first SELECT is made, right after the start transaction;, a snapshot is created. All future non-updating reads will be done from that snapshot.

Therefore, if you just naively SELECT after you do the update, it will pull the information from that original snapshot, which is before the row has been updated. By doing a SELECT ... FOR UPDATE you force it to get the correct information.

However, again, in a real application this could be an issue. Say, for example, your request is wrapped in a transaction, and after performing the update you want to output some information. Collecting and output that information may be handled by separate, reusable code, that you DON'T want to litter with FOR UPDATE clauses "just in case." That would lead to lots of frustration due to unnecessary locking.

Instead, you'll want to take a different track. You have many options here.

One, is to make sure you commit the transaction after the UPDATE has completed. In most case, this is probably the best, simplest choice.

Another option is to not try using SELECT to determine the result. Instead, you may be able to read the rows affected, and use that (1 row updated vs 0 rows update) to determine if the UPDATE was a success.

Another option, and one that I use frequently, as I like to keep a single request (like an HTTP request) fully wrapped in a single transaction, is to make sure that the first statement executed in a transaction is either the UPDATE or a SELECT ... FOR UPDATE. That will cause the snapshot to NOT be taken until the connection is allowed to proceed.

Let's reset our test database again and see how this works.

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);
Run Code Online (Sandbox Code Playgroud)

Connection 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      0 |                     NULL |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

Connection 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

Connection 2 is now waiting.

Connection 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0

SELECT * FROM table1 WHERE id = 1;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
commit;
Run Code Online (Sandbox Code Playgroud)

Connection 2 is now released.

Connection 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

Here you could actually have your server side code check the results of this SELECT and know it is accurate, and not even continue with the next steps. But, for completeness, I'll finish as before.

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
Run Code Online (Sandbox Code Playgroud)

Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0

SELECT * FROM table1 WHERE id = 1;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)
+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+
Run Code Online (Sandbox Code Playgroud)
commit;
Run Code Online (Sandbox Code Playgroud)

Now you can see that in Connection 2 the SELECT and SELECT ... FOR UPDATE give the same result. This is because the snapshot that the SELECT reads from was not created until after Connection 1 had been committed.

So, back to your original question: Yes, the WHERE clause is checked by the UPDATE statement, in all cases. However, you have to be careful with any SELECTs you may be doing, to avoid incorrectly determining the result of that UPDATE.

(Yes another option is to change the transaction isolation level. However, I don't really have experience with that and any gotchya's that might exist, so I'm not going to go into it.)