2018.07.03
MySQL でシンプルな排他制御を GET_LOCK で実現する!
次世代システム研究室の データストア 好きの Y.I. です。
MySQL/MariaDB/Percona Server でロックを取得して排他制御する際に便利なユーザーレベルロック GET_LOCK についてご紹介します。
DBで排他制御というとレコードロックやテーブルロックなどがあります。レコードロックにおいては、 SELECT FOR UPDATE で解放漏れなど排他制御を苦労されたことがあるかと思います。
ご紹介する GET_LOCK だと任意の文字列でロックを取得することができシンプルに排他制御が可能になります。イメージ的には MySQL に任意の文字列でフラグを立てて排他制御するイメージになります。
特に、ユーザー毎に排他制御をする要件に有効です。
例えば、ゲームのようなユーザーIDがありアイテムなどの消費や取得を厳しく排他制御したいケースなどです。
使い方
実行するコマンドは、最低3つになります。いずれも SELECT 文で実行します。
SELECT IS_FREE_LOCK('str'); ↓ SELECT GET_LOCK('str', 5); ↓ SELECT RELEASE_LOCK('str');
コマンドの説明
GET_LOCK(str, timeout)
文字列 str で指定された名前でロックの取得を試みます。
timeout秒の間ロックの取得が成功するのを待ちます。 timeout秒をすぎてロックが取得できない際に0が返却されます。
IS_FREE_LOCK(str)
文字列 str で指定された名前でロックがされていないか確認できます。
IS_USED_LOCK(str)
文字列 str で指定された名前ですでにロックされているか確認できます。ロックされている場合、ロックを取得したセッションIDが返却されます。
※IS_FREE_LOCK と IS_USED_LOCK はどちらを利用すればロック有無を確認できます。
RELEASE_LOCK(str)
GET_LOCK()で取得された str 名前のロックを1つ解放します。
特記事項
GET_LOCK実行先DBについて
GET_LOCKする接続先DBは、どのMySQLでもかまいません。MySQLデーモンに対して、 str でロックを取得します。DBが複数台ある状況では、GET_LOCKを実行するDBを1つのDBに決めておいて実行します。
GET_LOCKを複数回実行した場合について
MySQL では 5.7.5、 MariaDB では 10.0.2 から複数回のロック(GET_LOCK)が取得できるようになりました。以前は ロックを取得したdb connection から同じ文字列でロックを複数回実行してもロックの数は1つのままでした。以後はロック取得を実行した回数分のロックが取得されるようになり、ロック取得した回数分の解放実行が必要になります。
個人的には以前のような1つのロックの方が利用時のミスが少ない気がしますが、複数ロックを取得できることで同時に何件までOKのようなセマフォを実現できるので良いのかもしれません。
ロックの解放について
RELEASE_LOCK 実行時に解放されるのはもちろんのこと、ロックを獲得していた db connection が切断された際にも自動でロックを解放します。この db connection が切断された際にロックが解放されることで、ロックしたままとなってしまうようなロック解放漏れが発生しにくくなります。
ロック実行サンプル
2つの db connection を用意してロック取得してみます。 connection A(session id 2234662) で ロックを取得して10秒後に解放、 connection B(session id 2234836) では A がロック中にロックの取得を行い11秒待ち、A がロックの解放を行ったのとに B がロックを取得していることを確認します。
以下の順番で mysql へコマンドを実行します
◆A ロックされていないことを確認 MariaDB [mysql]> SELECT IS_FREE_LOCK('LOCK' ); +-----------------------+ | IS_FREE_LOCK('LOCK' ) | +-----------------------+ | 1 | +-----------------------+ 1 row in set (0.00 sec) ◆A ロックを取得 MariaDB [mysql]> SELECT GET_LOCK('LOCK', 11); +----------------------+ | GET_LOCK('LOCK', 11) | +----------------------+ | 1 | +----------------------+ 1 row in set (0.00 sec) ->return 1 はロック取得成功を表します ◆A 10秒間スリープ(=10秒間ロックを保持) MariaDB [mysql]> SELECT SLEEP(10); +-----------+ | SLEEP(10) | +-----------+ | 0 | +-----------+ 1 row in set (10.00 sec) △B ロックを取得、11秒間ウエイトしてAのロックが解放されるのを待つ MariaDB [mysql]> SELECT GET_LOCK('LOCK', 11); +----------------------+ | GET_LOCK('LOCK', 11) | +----------------------+ | 1 | +----------------------+ 1 row in set (4.53 sec) ->return 1 はロック取得成功を表します (=Aのロックが解放されBがロックを取得しました) ◆A ロックを解放 MariaDB [mysql]> SELECT RELEASE_LOCK('LOCK' ); +-----------------------+ | RELEASE_LOCK('LOCK' ) | +-----------------------+ | 1 | +-----------------------+ 1 row in set (0.01 sec) ◆A ロックしているsession idが B(2234836)であることを確認 MariaDB [mysql]> SELECT IS_USED_LOCK('LOCK' ); +-----------------------+ | IS_USED_LOCK('LOCK' ) | +-----------------------+ | 2234836 | +-----------------------+ 1 row in set (0.00 sec)
最後に
最近、 GET_LOCK を知らない方が意外に多いのかなと思う機会がありましたので、 GET_LOCK についてまとめました。 GET_LOCK は使い方がシンプルなため他の排他制御方法よりも有効なケースがあると思います。筆者は要件にもよりますが Oracle DB を利用していたシステムでは、SELECT FOR UPDATE を使うこともありましたが、 MySQL では使わずに排他制御アプリケーションを実現しています。MySQL で排他制御を検討する際に GET_LOCK もご検討してみてください。
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD