MySQL を使ったアドバイザリーロック
はじめに
アドバイザリーロックとは、並列処理で排他的に操作を行うためのロックの一種です。しかし、インターネット上のアドバイザリーロック解説記事は抽象的な情報が多く、わかりにくく感じました。そこで、この記事では具体的な例や図を交えながら MySQL のロック関数を使ってアドバイザリーロックを実現する例を紹介します。
MySQL のロック関数
アドバイザリーロックはファイルロックのようなシンプルな方法でも実現できます。しかし、ここでは MySQL がサポートしているロック関数 GET_LOCK
, RELEASE_LOCK
を使って実現します。これらのロック関数は、データベースと密接に結びつくトランザクションや、行ロックなどとは異なり、特定の行や特定のテーブルと結びつくことはありません。
ロック関数はとてもシンプルです。GET_LOCK
でロックを取り RELEASE_LOCK
でロックを解放します。具体例を見てみましょう。下記の SQL はキー broom
のロックを取得します。第二引数の 10
はタイムアウト時間です。
SELECT GET_LOCK("broom", 10)
ロックの取得が成功した場合は 1 を返します。10 秒待っても取得できない場合は 0 を返します。一度ロックを取得すると RELEASE_LOCK 関数によってロックを解放するまではロックの取得は失敗します。
SELECT RELEASE_LOCK("broom")
より詳しい解説は下記のページにあります。
ja: https://dev.mysql.com/doc/refman/8.0/ja/locking-functions.html
en: https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html
並列処理におけるロック関数
ロック関数は一つのスレッドで動作しているときはほとんど意味がありませんが、複数のスレッドで動作しているときには役に立ちます。このセクションでは具体例を紹介します。できれば、実際に2つのセッションを起動して実験してみると良いでしょう。Sequel Ace で二つのウィンドウを開いて操作すると簡単です。1つのセッションでは正しく動作しないので、必ず2つ以上のセッションで実験してください。
例1
下記のシーケンス図に概略を示します。二つの MySQL クライアントが一つのロック "broom"
を取り合っている図です。
MySQL クライアント Client1 がロックを取得している間は Client2 はロックを取得できません。しかし Client1 がロックを解放した後は Client2 はロックを取得することができます。
例2
上記の例と同じくロック broom
を取り合っている例です。ただし、今回は、6秒待ったタイミングでロックを解放しています。ロックが開放されるとすぐに、待ち状態だった GET_LOCK 関数がアクティブになり、ロック取得が実行されます。
ロックとパフォーマンス
上記の例1では Client2 が10秒間仕事をせずに待機していました。当然のことですが、この間 Client2 の CPU リソースは使われていないため、効率よく仕事をできていません。これは2人の掃除人がいて、1つの掃除用具を取り合っているような状況と置き換えるとわかりやすいでしょう。ロックを取りながら仕事をさせる場合は、パイプラインを作り、ロックが不要な仕事や、競合しない仕事を並列で実行できるようにすることが好ましいです。また、ロックを取る時間はなるべく短くしましょう。
ロックは通常では計算機リソースを消費しませんが MySQL サーバーではキューを消費しています。ロックが開放されたらすぐ仕事を再開できるようにコネクションを維持する必要があるからです。このキューが膨大になると MySQL に負荷がかかり、時にはクラッシュの原因になることがあるようです。そこまで並列度が高くなることは稀かと思いますが、サーバーにも一定の負荷がかかっているという点は気にかけておくのが良いと思います。
おわりに
この記事では MySQL におけるアドバイザリーロックの実現方法について説明しました。ロックの取得と開放のみに焦点を当てたため、ロックを保持しているクライアントアプリケーションの振る舞いについては述べていません。現実では、多くの場合、クライアントアプリケーションが何をするのかが問題となるでしょう。しかし、その問題に注力するためにも、ロックの振る舞いを頭に入れておくと良さそうです。
Discussion