web-dev-qa-db-ja.com

InnoDB行ロック-実装方法

私はmysqlサイトを読んでいますが、それがどのように機能するのか正確にはわかりません。

書き込みのために結果を選択して行ロックし、変更を書き込み、ロックを解放したい。 audocommitはオンです。

スキーム

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

ステータスが保留中のアイテムを選択し、作業中に更新します。排他的書き込みを使用して、同じアイテムが2回取得されないようにします。

そう;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

結果からIDを取得します

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

ロックを解除するために何かをする必要がありますか?それは上記のように機能しますか?

13
Wizzard

必要なのは、トランザクションのコンテキスト内から SELECT ... FOR UPDATE です。 SELECT FOR UPDATEは、UPDATEを実行しているかのように、選択された行に排他ロックをかけます。また、明示的に設定された分離レベルに関係なく、READ COMMITTED分離レベルで暗黙的に実行されます。 SELECT ... FOR UPDATEは同時実行性が非常に悪いため、絶対に必要な場合にのみ使用する必要があることに注意してください。また、人々がカットアンドペーストするときにコードベースで増加する傾向があります。

以下は、Sakilaデータベースのセッション例で、FOR UPDATEクエリの動作の一部を示しています。

まず、明確にするために、トランザクション分離レベルを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に戻ると、REPEATABLE READにいたため、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で更新されていることに注意してください。 REPEATABLE READではありません。READCOMMITTEDです。

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;

もう1つの副作用は、デッドロックを引き起こす可能性が大幅に増えることです。

あなたの特定のケースでは、おそらく以下が必要です:

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;

これが理にかなっているといいのですが。

26
Aaron Brown

InnoDBストレージエンジンを使用している場合は、行レベルのロックを使用します。マルチバージョニングと組み合わせて使用​​すると、特定のテーブルをさまざまなクライアントが同時に読み取り、変更できるため、クエリの同時実行性が向上します。行レベルの同時実行プロパティは次のとおりです。

異なるクライアントが同じ行を同時に読み取ることができます。

異なるクライアントが異なる行を同時に変更できます。

異なるクライアントが同じ行を同時に変更することはできません。 1つのトランザクションが行を変更した場合、最初のトランザクションが完了するまで、他のトランザクションは同じ行を変更できません。他のトランザクションも、READ UNCOMMITTED分離レベルを使用していない限り、変更された行を読み取ることができません。つまり、変更されていない元の行が表示されます。

基本的に、明示的なロックを指定する必要はありません。InnoDBはそれを処理しますが、状況によっては、明示的なロックの詳細を明示的に指定する必要がある場合があります。

次のリストは、使用可能なロックの種類とその影響を示しています。

[〜#〜]読み取り[〜#〜]

テーブルを読み取り用にロックします。 READロックは、テーブルからデータを取得するSELECTなどの読み取りクエリに対してテーブルをロックします。ロックを保持しているクライアントによっても、テーブルを変更するINSERT、DELETE、UPDATEなどの書き込み操作は許可されません。テーブルが読み取り用にロックされている場合、他のクライアントは同時にテーブルから読み取ることができますが、クライアントはテーブルに書き込むことができません。読み取りロックされているテーブルに書き込みたいクライアントは、現在テーブルから読み取っているすべてのクライアントが終了してロックを解放するまで待機する必要があります。

[〜#〜]書き込み[〜#〜]

書き込み用にテーブルをロックします。 WRITEロックは排他ロックです。テーブルを使用していない場合のみ取得できます。いったん取得されると、書き込みロックを保持しているクライアントのみがテーブルの読み取りまたは書き込みを行うことができます。他のクライアントは、読み取りも書き込みもできません。他のクライアントは、読み取りまたは書き込みのためにテーブルをロックできません。

ローカルの読み取り

テーブルを読み取り用にロックしますが、同時挿入を許可します。同時挿入は、「リーダーブロックライター」の原則の例外です。 MyISAMテーブルにのみ適用されます。 MyISAMテーブルの削除または更新されたレコードの結果として中央に穴がない場合、挿入は常にテーブルの最後に行われます。その場合、テーブルから読み取りを行っているクライアントは、READ LOCALロックでテーブルをロックして、読み取りロックを保持しているクライアントがテーブルから読み取りを行っている間に、他のクライアントがテーブルに挿入できるようにすることができます。 MyISAMテーブルに穴がある場合は、OPTIMIZE TABLEを使用してテーブルをデフラグすることで穴を削除できます。

2
Mahesh Patil

別の方法としては、最後に成功したロックの時刻を格納する列を追加し、その後、行をロックしたいものは、それがクリアされるか、5分(または何でも)経過するまで待つ必要があります。

何かのようなもの...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlockは、比較するのがより簡単(そしておそらくより高速)なUNIXタイムスタンプを格納するので、intです。

//すみません、実際に実行することは確認していませんが、実行しない場合は十分に近いはずです。

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

次に、更新された行数を確認します。行は2つのプロセスで一度に更新できないため、行を更新するとロックが取得されます。 PHPを使用していると仮定すると、mysql_affected_rows()を使用します。それからの戻りが1の場合、それは正常にロックされています。

次に、必要な処理を行った後にラストロックを0に更新するか、怠惰にして次のロック試行が成功するまで5分間待機します。

編集:時計が1時間戻るため、夏時間の変更の前後で期待どおりに機能することを確認するために少し作業が必要になる場合があります。 UNIXのタイムスタンプがUTCであることを確認する必要があります。

0
Steve Childs