I have a table tableName
which contains some names to be used:
id name in_use
1 name1 0
2 name2 0
My script get the first name from this table with in_use = 0
:
SELECT name FROM tableName WHERE in_use = 0 ORDER BY id
and immediately after set the row obtained to in_use = 1
:
UPDATE tableName set in_use = 1 WHERE id = '$idObtained'
When the script is finishing, it set in_use = 0
:
UPDATE tableName set in_use = 0 WHERE id = '$idObtained'
The problem is that when the script will be executed more than once in the same instant, two or more selects may get the same name form this table.
My aim is to avoid two scripts to get the same row in their execution.
There is a way to do that with PHP / MySQL?
Does locking the table before the select and unlocking it after the update do that?
The script (the code is only a draft. I'll develop it only if what I need is possible in PHP and MySQL):
<?php
// ...
// TODO: lock the raw, if found
$query = "SELECT id, name
FROM tableName
WHERE in_use = 0
ORDER BY id";
$stmt = $connection->prepare($query);
$stmt->execute();
$data = $stmt->fetch();
if(!empty($data['id'])) {
$query = "UPDATE tableName
SET in_use = 1
WHERE id = '$data[id]'";
$stmt = $connection->prepare($query);
$stmt->execute();
// do some with $data['name]
$query = "UPDATE tableName
SET in_use = 0
WHERE id = '$data[id]'";
$stmt = $connection->prepare($query);
$stmt->execute();
// TODO: unlock the raw
}
?>
Thanks a lot.
To fix your approach
BEGIN;
SELECT id, name FROM tableName WHERE in_use = 0 ORDER BY id
FOR UPDATE; -- Add this
$idObtained = ...
UPDATE tableName set in_use = 1 WHERE id = '$idObtained';
COMMIT;
Later, after finishing the work with that row:
UPDATE tableName set in_use = 0 WHERE id = '$idObtained';
Discussion:
The FOR UPDATE
prevents another connection from sneaking in between the SELECT
and the UPDATE
and also grabbing that row.
If the action on this row will take only a few seconds, this technique is overkill. So, I am assuming it will take a long time, hence two separate transactions -- one to 'grab' the row, and another to 'release' it.
Note that I did not put BEGIN
and COMMIT
around the 'release' UPDATE
. Any standalone query is a transaction unto itself (assuming autocommit=ON
). Hence, the release is easy to do with just that single query.
Alternative approach
It may be possible to 'simplify' the 'grab', if we can devise a single instruction that both looks for a 'free' row and 'grabs' it. One approach includes an extra column saying which connection 'owns' each grabbed row. But, let's simply change how set_in_use
works. Assuming you are not going to grab more than one row per connection, the CONNECTION_ID()
is convenient:
$conn = `SELECT CONNECTION_ID()`
UPDATE tbl SET in_use_by = $conn
WHERE in_use_by IS NULL
ORDER BY id
LIMIT 1; -- grab only 1 row
SELECT ... FROM tbl WHERE in_use_by = $conn; -- Get the data to process
... process ...
UPDATE tbl SET in_use_by = NULL WHERE in_use_by = $conn;
With this technique, no explicit transactions are needed. autocommit=ON
works just fine.
This technique gives you the ability to see who has what items. (Not that you can do much with such knowledge.)
(There are probably other approaches.)