必要なのは、トランザクションのコンテキスト内からのSELECT ... FOR UPDATEです。SELECT FOR UPDATEは、UPDATEを実行しているかのように、選択した行に排他ロックをかけます。また、分離レベルが明示的に設定されているものに関係なく、READ COMMITTED分離レベルで暗黙的に実行されます。SELECT ... FOR UPDATEは同時実行性に非常に悪いため、絶対に必要な場合にのみ使用してください。また、ユーザーがカットアンドペーストすると、コードベースが増加する傾向があります。
次に、FOR UPDATEクエリの動作の一部を示す、Sakilaデータベースのサンプルセッションを示します。
まず、非常に明確であるため、トランザクション分離レベルをREPEATABLE READに設定します。これはInnoDBのデフォルトの分離レベルであるため、通常は不要です。
session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA | WILLIAMS |
+------------+-----------+
1 row in set (0.00 sec)
他のセッションでは、この行を更新します。リンダは結婚し、名前を変更しました。
session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
セッション1に戻ると、繰り返し読み込めていたため、LindaはまだLINDA WILLIAMSです。
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA | WILLIAMS |
+------------+-----------+
1 row in set (0.00 sec)
しかし今、この行への排他的アクセスが必要なので、行に対してFOR UPDATEを呼び出します。行の最新バージョンが取得されることに注意してください。このバージョンは、このトランザクションの外部でsession2で更新されました。それは繰り返しの読み取りではなく、コミットの読み取りです
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA | BROWN |
+------------+-----------+
1 row in set (0.00 sec)
session1で設定されたロックをテストしてみましょう。session2は行を更新できないことに注意してください。
session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
しかし、私たちはまだそれから選択することができます
session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address |
+-------------+------------+-----------+------------+-------------------+
| 3 | LINDA | BROWN | 7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)
そして、外部キー関係で子テーブルを更新できます
session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session1> COMMIT;
別の副作用は、デッドロックを引き起こす可能性を大幅に高めることです。
あなたの特定のケースでは、おそらくあなたが欲しい:
BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;
「他の処理を行う」部分が不要で、実際に行に関する情報を保持する必要がない場合、SELECT FOR UPDATEは不要で無駄であり、代わりに更新を実行するだけで済みます。
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;
これが意味をなさないことを願っています。
items
WHEREstatus
= 'pending' LIMIT 1 FOR UPDATE;」で2つのスレッドが入ってくると、私の問題は解決しないようです。両方が同じ行を見ると、一方が他方をロックします。ロックされた行をバイパスして、保留中の次のアイテムに移動できることを望んでいました。