BizStationブログスマートフォン用の表示で見るBizStationブログビズステーション株式会社の公式ブログです。この広告は、90日以上更新していないブログに表示しています。Transactd今回から数回にわたり、TransactdのオペレーションとInnoDBにおけるロックについて解説します。ロックについてはあまり良くわからなくてもとりあえずそれなりに動くアプリケーションは作れてしまいます。マルチユーザー環境でミッションクリティカルなアプリケーションを書くには、ロックの理解が不可欠です。ロックをうまく使って、矛盾や間違いのない読み書きをしつつ同時実行性も高いアプリケーションにしましょう。その1では、Transactdを実装する上でMySQLのソースやドキュメントから得た知見を基に、InnoDBのロックの種類と分離レベルに応じてそれをどのように使うかをまとめてみます。説明に入る前に、事前にMySQLのトランザクション関連用語の理解が必要です。下記の用語についてあまりよくわからない場合は、大まかで良いので理解しておいてください。READはネクストキーロックという仕組みによってファントムリードが起きないようになっています。トランザクション分離レベルは4つありますが、READ_UNCOMMITEDはほとんど使用されないので、ここでは省いて説明します。InnoDBのロックはじめにInnoDBの基本的なロックについて説明します。InnoDBはMVCCとこれらのロックを使って、トランザクション分離レベルを実装しています。locking)InnoDBは行ロックをサポートしています。lock(排他ロックX)の2種類があります。トランザクションT1によって共有ロック(S)が取得されたレコードRは、別のトランザクションT2から共有ロック(S)はできますが、排他ロック(X)はできません。トランザクションT1によって排他ロック(X)が取得されたレコードRは、別のトランザクションT2から共有ロック(S)も排他ロック(X)もできません。2つのトランザクションが同じレコードのロックを取得しようとしたときのロックの可否を表したのがInnoDBソースコードの以下の部分です。「TRUE」の組み合わせでは互いに競合せず双方とも取得できます。「FALSE」の組み合わせでは、後から取得しようとした方は、先に取得されたロックが解放されるまでロックを取得することができません。説明していないIXやISというロックがありますが、ここではXとSの組み合わせのみ見てください。FALSE}};共有ロックと排他ロックそれぞれの使い道は以下の通りです。GAPロックGAPロックは、レコードとレコードの間の空間をロックします。これでInsertをブロックし、行ロックで更新をブロックすることで、ファントムリードを防止します。具体的には、後ろ側のレコードにGAPロックを行います。GAPロックは単体で使用する場合と、行ロックと組み合わせて使用する場合があります。lock)と呼んでいます。GAPロック単体、ネクストキーロック、GAPロックを含まない通常の行ロックの3種類をinnodb_monitorで見ると、下記のように表示されます。gap読み取ったレコードすべてにネクストキーロックを行い、最後に読み取ったレコードにGAPロックかネクストキーロック*2を行うことで、検索範囲内の更新と挿入ブロックを実現します。1から15までレコードが挿入されたテーブルがあるとします。1から10まで10個のレコードは確実にネクストキーロックが行われます。しているので読まなくても検索終了を判断できそうなのですが、11もネクストキーロックされます。(11のロックについては、インデックスや検索条件の書き方によって変わります。MySQLでは、ほぼロックされると考えてください。)GAPロック単体でロックされたレコードは、前の空間がロックされているだけでレコードそのものはロックされていません。1:で示されたフィールドの内容が同じ)に対して異なる2つのトランザクションがGAPロック(X)と共有ロック(S)を取得しています。RECORDSまで到達した場合は、GAPロックを明示するレコードが存在しません。その場合は、最後のレコードのさらに後ろにある終端を示すレコード(supremum)がロックされます。READかSERIALIZABLEが指定されていると、行ロックに替えてネクストキーロックが使用されます。余談ですが、SQLの悩みに「ロックされるレコードの範囲はどこになるのかがわかりにくい」といったものがあります。読み取り方法や範囲をオプティマイザが決めるためです。Transactdの場合はそのような悩みはありません。それらはコードを書く人が決めるからです。(複数粒度ロック)とMySQLのドキュメントに記されていますが、実際にこのようなロックがあるわけではありません。テーブルにこのロックを明示すると、以降の行操作に対して、行の共有ロック(S)または排他ロック(X)の取得が行われる仕組みのことです。行ロックに合わせて、インテンションロックにも共有ロック(IS)と排他ロック(IX)の2種類があります。ともにテーブルに対してロックを明示します。共有ロック(IS)は、トランザクションT内でテーブルtの複数の行に対して共有ロック(S)の取得を試みる。排他ロック(IX)は、トランザクションT内でテーブルtの複数の行に対して排他ロック(X)の取得を試みる。インテンションロックの使われ方を簡単に説明すると、これから更新や削除を行う際には排他ロック(IX)を設定し、更新や削除をされたくない読み取りを行う際には共有ロック(IS)を設定します。(SQL文で明示的にインテンションロックを操作する方法はありません。MySQLがSQL文からテーブルロックタイプを決め、そのタイプからInnoDBがインテンションロックタイプを決定します)Multi-VersioningInnoDBの各レコードには、最後に追加または更新・削除したトランザクションのIDが記録されます。更新・削除によって古くなったバージョン(旧バージョン)のレコードは、テーブルスペースのロールバックセグメントに保持されます。スナップショットは、自分のトランザクションIDと同じか、より前のトランザクションIDで最後にコミットされたレコードの集合。スナップショットの読み取りはロックは不要。ロックの取得や変更はできない。前述の共有ロック(S)と排他ロック(X)のいずれでもないLOCK_NONE(ロックなし)の読み取りは、スナップショットが使われます。reads(一貫性読み取り)と言います。売上伝票のヘッダーと明細はトランザクションによって一式でアトミックな書き込みが行われるとします。読み取りにおいて一貫性がないと、ヘッダーと明細で異なるバージョンを読んでしまうことが起こります。T1がヘッダーを読み取り、次に明細を読み取る前に、T2によって変更された伝票がコミットされた場合などです。readsはアトミックな特性と併せて、このような問題を解決するためにとても重要な機能です。readsには、2つの方法があります。Readsはマルチバージョン機能を使って、トランザクションを開始した時点のスナップショットにアクセスすることで実現します。readsを実現します。Readsを行うには、ロックを要求しない読み取りを行うことで実現します。Readsは行えません。以下は、SERIALIZABLEがLOCK_Sを強要する部分のInnoDBのコードの抜粋です。Readsには2つ注意点があります。READ-COMMITEDの場合は、スナップショットのバージョンがトランザクション内で変わる。スナップショットは更新できない。更新は最新バージョンに対して行うが、スナップショットとは値が異なることがある。REPEATABLE-READのときは、トランザクションの最初から最後まで同じスナップショットのバージョンです。ところが、READ-COMMITEDのときは1つのSQL文の中でのみ同じものです。トランザクション中であっても、1文目と2文目では異なるスナップショット(のバージョン)が使われる可能性があります。Reads)スナップショットはコミット済の確定した内容なので、その行をロックして変更することはできません。トラザクション内で更新が必要な場合は、スナップショットではなく、最新バージョンをロックする必要があります。最新のバージョンが、自分のトランザクションIDより新しいものであった場合、その内容はスナップショットとは異なるものになります。Readsとの混合は、REPEATABLE-READを満たすことができません。下図は、REPEATABLE-READのトランザクションにおいて、スナップショットの様子を図にしたものです。3)によって更新された最新のバージョンを読み取って排他ロック(X)を取得しなくてはなりません。Readsで読み取ったバージョンとは異なります。絶対に、ロックなしREADによって得られた値を元に更新してはいけません。ReadsというワードはMySQLのドキュメントにはありませんが、Nonlockingと対比するために便宜的にこう呼んでいます。readを実現する方法です。(この方法は、更新を行いつつ一貫性のある読み取りができますが、ロックが多くなるためトランザクションの同時実行性能は大きく低下してしまいます。指定するとロックを指定していない読み取りにおいても、ネクストキーロック(S)を自動で取得します。使ってロックを指定しないと読み取りにロックをかけません。Readsにするには明示的にロックを行う必要があります。ちなみに、REPEATABLE-READですべての読み取りにロックを指定をすると、SERIALIZABLEと同じになります。READ_COMMITEDでは、ロック指定読み取り時にネクストキーロックに替えて通常の行ロックが使用されます。Readsにはなりません。ロック付読み取り時のロック対象クエリーにマッチしないレコードのロック保持と開放について説明します。クエリーの検索対象は、インデックスを使った特定のレコード範囲(または複数の範囲)、または全レコードのどちらかです。その際に、範囲内ではあるけれども条件にマッチしないレコードのロックはどうなるのでしょうか?それは分離レベルによって以下のようになります。ロックを解放この保持するか否かの実装は、InnoDBのha_innobase::unlock_row()内にあります。MySQLもTransactdもアンマッチの時は、unlock_row()を呼び出しますが、InnoDBがREPEATABLE-READとSERIALIZABLEのときは無視してロックを解放しないようになっています。以下はそれを実装するInnoDBのソースコード抜粋です。}row_read_typeがROW_READ_WITH_LOCKSであるときは、直前の読み取りでロックをしていることを示します。row_unlock_for_mysql()が呼び出されません。MySQLのロック制御ここまではInnoDBのロック機能について説明しました。それとは別にMySQLにはテーブルロック機能があります。MySQLがどのように、InnoDBのロックを制御するか説明します。statementは、InnoDBのロックとは無関係です。TABLESは、MySQLの管理するテーブルロックマネージャの機能を利用して実現するものです。MySQLのテーブル操作はhandlerインターフェースを通じて行いますが、handlerインターフェースでテーブルにアクセスするには事前に必ずテーブルのロックを取得しなければなりません。WRITE不可)を取得します。以降このテーブルはREADロックの取得のみ許可されます。WRITE不可)を取得して、以降このテーブルのすべてのロック取得をブロックします。読み取りも書き込みもできなくなります。TABLESで開放されます。TABLESは、行レベルのロックではくテーブル単位のロックなので、書き込みのある複数トランザクションの同時実行はできません。分離レベルの概念もなくトランザクションは占有です。TABLES...で自動で開始されています。AUTOCOMMITが有効だと文の終了でCOMMITされ、後でROLLBACKできなくなってしまいます。無効にしておきましょう。TABLESの前にCOMMITまたはROLLBACKすることで、アトミックな処理ができます。Transactdでテーブルをロックする少しだけTransactdでの処理を説明します。TD_OPEN_READONLY_EXCLUSIVEを指定することで、WRITEまたはREADのテーブルロックを行うことができます。TABLES呼び出し時にすべてのテーブルの列挙が必要で、後からテーブルを追加したりできませんが、Transactdは後からいくつでも追加できます。通常モードとEXCLUSIVEモードを混在させてオープンすることもできます。それらのテーブルが混在したトランザクションもそのまま行えます。SQL文にはアトミックな処理に独特の癖がありますが、Transactdにはそのような特別なことは何もありません。テーブル1を排他ロックでオープン。テーブル2をREADロックでオープン。分離レベルに応じてInnoDBがどのように読み取り時にロックをするかを表にまとめます。MODE(S)を使った読み取り)がありますので、それぞれをトランザクション中に使うとどのような読み取り処理をするかが整理できます。併せてロック有りの際にアンマッチレコードのロック開放を行うかどうかも加えておきました。TRANSACTIONで開始しCOMMITで終了するトランザクションと考えてください。(1文での自動トランザクションも基本的には同じですが、更新を含まない場合、ロックをしないなどの最適化がMySQL内部で自動的に行われます。するなお、更新や削除を行う場合は、対象レコードの読み取り時に自動で排他ロック(X)の取得がトライされ、取得できれば実行されます。まとめここまで見てきたInnoDBの仕様やロックの特性から、MySQLのSQL文でミッションクリティカルなアプリケーションを書く場合に大切なことをまとめます。分離レベルのデフォルトは、REPEATABLE-READにする。COMMIT)で行う。COMMIT)で読み取る。MODE(S)を使って明示的しなければ行われない。分離レベルをSERIALIZABLEにすれば、明示しない読み取りも共有ロック(S)が取得できる。(REPEATABLE-READの)トランザクション内において、ロックなし読み取りと更新処理の混在に注意する。この組み合わせは、REPEATABLE-READではなく、異なった値を読むことがある。受け入れられない場合は、すべての読み取りをロック付読み取りにするか、SERIALIZABLEにする。ファントムリードが問題にならないトランザクションであれば、同時実行性を向上するためにREAD-COMMITEDを使う。TABLESを呼び出す。テーブルロックはテーブルを占有してしまうので、マルチユーザー環境では特別な状況以外使用してはいけない。いかがだったでしょうか?これらを理解し実践することがミッションクリティカルなアプリケーションの第一歩になると思います。何か参考になればいいなと思います。次回、その2ではTransactdのInnoDBロック制御について詳しく書きたいと思います。ORMをリリースしました。ツイートリンクビズステーションWebサイトはてなブログTransactdはてなブログをはじめよう!bizstationさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。