web-dev-qa-db-ja.com

MySQLで楽観的ロックを正しく実装する方法

MySQLに楽観的ロックを正しく実装するにはどうすればよいですか?

私たちのチームは、以下の#4を実行する必要があると推定しました。そうしないと、別のスレッドが同じバージョンのレコードを更新できるリスクがありますが、これが最善の方法であることを検証したいと考えています。

  1. 楽観的ロックを使用するテーブルにバージョンフィールドを作成します。列名= "バージョン"
  2. 選択時には、必ずバージョン列を含めてバージョンを書き留めてください
  3. その後のレコードの更新時に、更新ステートメントは「where version = X」を発行する必要があります。ここで、Xは#2で受け取ったバージョンであり、その更新ステートメント中のバージョンフィールドをX + 1に設定します
  4. SELECT FOR UPDATE更新しようとしているレコードを変更して、更新しようとしているレコードを誰が変更できるかをシリアル化します。

明確にするために、同じバージョンのレコードを取得する同じタイムウィンドウで同じレコードを選択する2つのスレッドが、レコードを同時に更新しようとした場合に、お互いに上書きされないようにします。 #4を行わない限り、両方のスレッドが同時にそれぞれのトランザクションに入る場合(ただし、まだ更新を発行していない場合)、更新に移動すると、UPDATEを使用する2番目のスレッドが可能性があると考えています...バージョン= Xは古いデータで動作します。

バージョンフィールド/楽観的ロックを使用している場合でも、更新時にこの悲観的ロックを実行する必要があると私たちは考えていますか?

13
BestPractices

あなたの開発者は間違っています。 どちらかSELECT ... FOR UPDATEまたは行のバージョン管理が必要です。両方ではありません。

試してみてください。同じデータベースに対して3つのMySQLセッション(A)(B)(C)を開きます。

(C)の問題:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;

(A)(B)の両方で、行のバージョンをテストおよび設定するUPDATEを発行し、それぞれのwinnerテキストを変更して、どのセッションがどれであるかを確認できるようにします。

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;

(C)UNLOCK TABLES;でロックを解除します。

(A)および(B)は行ロックを求めて競合します。それらの1つが勝ち、ロックを取得します。もう一方はロックをブロックします。ロックを獲得した勝者は、行を変更します。 (A)が勝者であると仮定すると、SELECT * FROM test WHERE id = 1を使用して、変更された行を確認できます(まだコミットされていないため、他のトランザクションからは見えません)。

勝者セッションのCOMMITで、(A)と言ってください。

(B)はロックを取得して更新を続行します。ただし、バージョンは一致しなくなったため、行カウント結果で報告されるように、行は変更されません。影響を与えたのは1つのUPDATEだけであり、クライアントアプリケーションは、どのUPDATEが成功し、どれが失敗したかを明確に確認できます。それ以上のロックは必要ありません。

ここのPastebinのセッションログを参照 。セッション間の違いを見分けやすくするために、mysql --Prompt="A> "などを使用しました。時間順にインターリーブされた出力をコピーして貼り付けたので、完全に生の出力ではなく、コピーと貼り付けでエラーが発生した可能性があります。自分でテストして確認してください。


notに行バージョンフィールドを追加した場合、その後確実に確実にするには、SELECT ... FOR UPDATEを実行する必要があります。注文。

考えてみると、UPDATEからのデータを再利用せずにSELECTをすぐに実行している場合、または使用している場合、SELECT ... FOR UPDATE完全に冗長です行のバージョン管理。 UPDATEはとにかくロックを取得します。他の誰かが読み取りと次の書き込みの間に行を更新すると、バージョンが一致しなくなるため、更新は失敗します。これが楽観的ロックの仕組みです。

SELECT ... FOR UPDATEの目的は次のとおりです。

  • デッドロックを回避するためにロックの順序を管理します。そして
  • 行からデータを読み取りたいときに行ロックのスパンを拡張するには、アプリケーションでそれを変更し、SERIALIZABLE分離または行のバージョン管理を使用する必要なく、元の行に基づく新しい行を書き込みます。

楽観的ロック(行のバージョン管理)とSELECT ... FOR UPDATEの両方を使用する必要はありません。どちらかを使用してください。

17
Craig Ringer
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

ロックは必要ありません(テーブルではなく、トランザクションではありません)。

  • UPDATEはアトミックです
  • LAST_INSERT_ID()はセッション固有であるため、スレッドセーフです。
0
Rick James