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


12

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

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

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

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

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


どうしたの?UPDATEでバージョン番号を増やすと、バージョン番号が読み取られたときと同じではないため、2回目のUPDATEは失敗します。
AndreKR

確かですか?トランザクション分離レベルを特定の設定に設定しない限り、他のスレッドの更新が実際に表示されることは不明確です。両方が同時にトランザクションに入る場合、2番目のスレッドは更新を行うときにOLDデータを非常によく見ることができます。MySQLはACIDアリーナでOracleほど堅牢ではないため、ダーティリード/更新を防止する楽観的ロックをMySQLに実装するためのベストプラクティスの方法を探しています。
BestPractices 2012年

しかし、トランザクションはとにかくコミット中に失敗しますよね?
AndreKR

この状況に対処するために、select for updateを実行する必要があることを示しています。dev.mysql.com
doc

@BestPractices 行のバージョン管理による楽観的ロックのいずれか、 SELECT ... FOR UPDATEまたは両方が必要です。回答の詳細を参照してください。
クレイグリンガー

回答:


16

あなたの開発者は間違っています。両方ではなく、いずれか 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)ロックを取得し、更新を続行します。ただし、バージョンは一致しなくなったため、行カウント結果で報告されるように、行は変更されません。UPDATE効果があったのは1つだけで、クライアントアプリケーションはUPDATE、どちらが成功し、どれが失敗したかを明確に確認できます。それ以上のロックは必要ありません。

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


あなたがいた場合ではない行バージョンフィールドを追加し、その後、あなたはする必要があるだろうSELECT ... FOR UPDATE確実に順序を確保できるようにします。

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

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

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

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


クレイグに感謝します。あなたは正しかった-開発者は間違っていました。このテストを実行していただきありがとうございます。
BestPractices 2012年

SQLサーバーはどうですか?トランザクション分離レベルとは関係なく、更新された行で常にロックが取得されますか?
plalx 2014年

@plalxさて、ドキュメントは何と言っていますか?このようなインタラクティブテストを実行するとどうなりますか?
クレイグリンガー

@ CraigRinger、BがAのコミット前に、Aの更新後にロックを取得するとどうなりますか?
MengT 2017年

1
@MengTできません。ロックだからです。
クレイグリンガー2017年

0
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()はセッション固有であるため、スレッドセーフです。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.