InnoDBの行ロック-実装方法


13

私は今、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>

ロックを解除するために何かする必要がありますか?

回答:


26

必要なのは、トランザクションのコンテキスト内からの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;

これが意味をなさないことを願っています。


3
ありがとう。「SELECT id FROM itemsWHERE status= 'pending' LIMIT 1 FOR UPDATE;」で2つのスレッドが入ってくると、私の問題は解決しないようです。両方が同じ行を見ると、一方が他方をロックします。ロックされた行をバイパスして、保留中の次のアイテムに移動できることを望んでいました。
Wizzard12年

1
データベースの性質は、一貫したデータを返すことです。値が更新される前にそのクエリを2回実行すると、同じ結果が返されます。「行がロックされていない限り、このクエリに一致する最初の値を取得してください」というSQL拡張機能はありません。これは、リレーショナルデータベースの上にキューを実装しているように思われます。そうですか?
アーロンブラウン

アーロン; はい、それは私がやろうとしていることです。私はギアマンのようなものを使用することを見てきました-しかし、それはバストでした。他に何か考えていることはありますか?
ウィザード

engineyard.com/blog/2011 / ...を読むべきだと思います-メッセージキューについては、選択したクライアントの言語に応じて多くのメッセージがあります。ActiveMQ、Resque(Ruby + Redis)、ZeroMQ、RabbitMQなど
アーロンブラウン

セッション1の更新がコミットされるまで、セッション2が読み取りをブロックするようにするにはどうすればよいですか?
CMCDragonkai

2

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

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

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

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

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

次のリストは、使用可能なロックの種類とその効果について説明しています。

読んだ

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

書く

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

ローカルで読む

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


答えてくれてありがとう。このテーブルと100個のクライアントが保留中のアイテムをチェックしているため、多くの衝突が発生していました。2〜3個のクライアントが同じ保留中の行を取得しています。テーブルのロックは遅くなります。
ウィザード

0

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

何かのようなもの...

Schema

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

lastlockはintであり、unixタイムスタンプを比較するのがより簡単な(そしておそらくより速い)として保存します。

//セマンティクスを失礼します。私はそれらが実際に実行されることを確認していませんが、実行しない場合は十分に近いはずです。

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であったことを確認する必要があります-これはとにかくかもしれません。


-1

または、レコードフィールドをフラグメント化して並列書き込みを許可し、行ロックをバイパスできます(フラグメント化されたjsonペアスタイル)。したがって、複合読み取りレコードの1つのフィールドが整数/実数である場合、そのフィールドのフラグメント1〜8(8つの書き込みレコード/行が有効)を持つことができます。次に、個別の読み取りルックアップへの書き込みごとにフラグメントをラウンドロビンで合計します。これにより、最大8人の同時ユーザーを同時に使用できます。

部分的な合計を作成する各フラグメントでのみ作業しているため、衝突や真の並列更新はありません(つまり、統合読み取りレコード全体ではなく、各フラグメントをロックします)。これは明らかに数値フィールドでのみ機能します。結果を保存するために数学的な修正に依存する何か。

したがって、統合読み取りレコードごとの統合読み取りフィールドごとの複数の書き込みフラグメント。これらの数値フラグメントは、ECC、暗号化、およびブロックレベルの転送/保存にも役立ちます。書き込みフラグメントが多いほど、飽和データに対する並列/同時書き込みアクセス速度が向上します。

MMORPGはこの問題で大きな影響を受けます。多くのプレイヤーがArea of​​ Effectスキルでお互いにぶつかり合うときです。これらの複数のプレーヤーはすべて、他のすべてのプレーヤーをまったく同時に同時に並行して書き込み/更新する必要があり、統合されたプレーヤーレコードに書き込み行ロックストームを作成します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.