今回は、前回の「チューニングについて」に続いて「ロック」について解説します。
■1.エンキューについて
それではエンキューについて説明します。
エンキューとは、共有メモリ以外の共有リソースを排他制御するためのロック・メカニズムです。これは、キュー・メカニズムに基づいてアクセス制御されます(つまり、最初に要求したプロセスから獲得するように制御します)。エンキューの数も沢山あるので、代表的な(良く発生するような)エンキューを取り上げて説明しようと思います。
(1)HWエンキュー
これはテーブルなどのHWM(使用済みブロックの最後の位置)を引き上げするときに獲得するエンキューです。つまり、INSERT文などで多量にデータを挿入するような場合には、エクステント・サイズが小さいと頻繁にエクステントを追加することになり、エンキュー獲得時間が長くなります。そのため、多くのINSERT文を並列実行するような場合に発生してしまいます。これは、エクステント・サイズを大きくすることで、エクステント追加の回数を減らすことができるため、エンキュー待ちの発生を削減することが可能です(エクステントについては、第8回「断片化について」で少し説明しましたが、頻繁に追加されるときの待機イベントまでは説明していませんでした)。
(2)SQエンキュー
これはシーケンス(順序)を取得するのときに獲得するエンキューです。シーケンスは、トランザクション・ロックを使用せずに高速に連番を生成することができる機能です。ただし、取得した順序番号をロールバックしてしまうと、その番号は使用されません(トランザクション・ロックを使用しないためコミットする前に次の番号を採番するので欠番になります)。これも多くのシーケンスを使用するINSERT文を並列実行するとトランザクション・ロックは獲得しないとは言っても待ちが発生してしまいます。その場合はCACHEパラメータを大きくすることで待ちを少なくすることができますので検討して下さい。ただし、キャッシュされた値のうち、コミットされたトランザクションで使用されなかった番号はシステム障害などが発生すると失われますので(欠番になりますので)注意して下さい。これは高速化するときのトレードオフを忘れてはいけないという良い例です。
(3)TXエンキューとTMエンキュー
TX(トランザクションロック)とTM(DMLエンキュー)は、もう少し分かりやすくいうと行ロック(TX)と表ロック(TM)のことです。TMロックは以下のようにSELECT文以外では必ず獲得されます(Oracleデータベースは共有ロックを使用しないのでSELECT文では獲得されません)。あまり気にしていないかもしれませんが、DML(INSERT, UPDATE, DELETE)文を行っても必ず行排他(RX)モードで獲得されます(これは行レベルで更新を行うという意味です)。どうして表ロックを行う必要があるかというと、データを更新中(行ロック取得中)に表全体をロックする処理(DDLなど)の実行を防止するためです(このときに表ロックを獲得できるかだけで確認が可能だからです)。RXに対してRXは獲得できますのでTMエンキュー待ちは発生しませんが、このときの行レベルの排他制御はTXエンキューで行われるということです(ロック・モードについてはv$lockで確認できます)。ただし、TXエンキューは行毎に割当てられるものではなく(行レベルはブロック内のITLで管理されます)、トランザクション毎に割当てられるものですので間違わないようにして下さい(トランザクションがどのトランザクションの行ロック待ちかを管理します)。これの解消は、アプリケーションを見直す必要がります(ロックの保存期間を短くするなど)。ロックについては「2.トランザクションの分離レベル」でもう少し説明します。
| 取得するモード | SELECT…FROM… SELECT…FOR UPDATE… INSERT INTO… UPDATE… DELETE FROM… LOCK TABLE…SHARE MODE LOCK TABLE…SHARE ROW EXCLUSIVE MODE LOCK TABLE…EXCLUSIVE MODE DDL |
なし RS RX RX RX S SRX X X |
||||||
| RS | RX | S | SRX | X | ||||
| 取 得 済 み モ | ド |
RS(行共有) | ○ | ○ | ○ | ○ | × | ||
| RX(行排他) | ○ | ○ | × | × | × | |||
| S(共有) | ○ | × | ○ | × | × | |||
| SRX(共有行排他) | ○ | × | × | × | × | |||
| X(排他) | × | × | × | × | × | |||
SELECT…FOR UPDATE…は、Oracle Database 9iR2(9.2.0.6以上)及びOracle Database 10gR1(10.1.0.4以上)からRX(行排他モード) に変更になっております。
(4)STエンキュー
これは領域管理トランザクション・エンキューです。このエンキュー待ちは、エクステントの領域管理(獲得と開放処理など)が頻繁に行われた時に発生します。これは領域管理がディクショナリ管理の時にはデータ・ディクショナリ(FET$やUET$)を更新するためにSTエンキューを獲得する必要があるために発生しますが、ローカル管理では発生しませんので、ローカル管理表領域を使用して下さい(これも第8回で説明しましたね)。
■2.トランザクションの分離レベル
最後に、トランザクションの競合に関係するトランザクションの分離レベルについて説明します。
(1)トランザクション・ロックについて
トランザクションは、データを他のトランザクションから保護するために(データの一貫性のために)、ロックの機能を使用します。ロックを多用すると他のトランザクションはデータにアクセスすることができないために並列性が低下します(そのため、どこまでロックするかはパフォーマンスに大きく影響します)。このロックには、読込みを行う前に獲得する読込みロック(同時に複数獲得できることから共有ロックとも呼ぶ)と書込みを行う前に獲得する書込みロック(同時に獲得できないことから排他ロックとも呼ぶ)があります。以下の図のように、読込みロック中には書込みロックだけが獲得できませんが、書込みロック中には何も獲得することができません。

ロックの獲得/解放は2相ロッキング(ロックはデータをアクセスする直前で獲得し、トランザクション終了後に解放する)を使用しているため、リソースはトランザクションが終了するまで解放されません。そのため、同一データを多数のユーザがアクセスするときに、ロックの保持時間が長い場合には競合が発生し易くなります(つまり、トランザクションが長いと競合が発生し易くなりますが、短くすると整合性が失われます。これはSQエンキューで説明した欠番が発生するということです)。ただし、読込みロックについては並列性の向上のため、2相ロッキングを使用しないようにできます(保持期間をトランザクションの分離レベルで指定可能になっています)。
(2)分離レベルについて
ANSI/ISOの分離レベル(Isolation)はロックの保持期間によってデータに矛盾が発生する3つ現象と、それらの矛盾をどこまで許すか(どこまでデータの一貫性を犠牲にしてよいか)で4つの分離レベルを定義しています(分離レベルと矛盾の現象の関係は以下の表になります)。これが読取りに対する一貫性のレベルになります(読取りについては、アプリケーションしだいで不整合にならない場合があるので、指定可能になっています)。
| 矛盾の現象 分離レベル |
ダーティ・リード | 非リピータブル・リード | ファントム・リード |
| READ UNCOMITTED | 発生する | 発生する | 発生する |
| READ COMMITTED | 発生しない | 発生する | 発生する |
| REPEATABLE READ | 発生しない | 発生しない | 発生する |
| SERIALIZABLE | 発生しない | 発生しない | 発生しない |
この分離レベルはトランザクション開始前に以下のSQL文で設定します。
SQL> SET TRANSACTION ISOLATION LEVEL <分離レベル> ;
これによりトランザクション毎に発生させたくない矛盾を排除することが可能となります(トランザクションが「REPEATABLE READ」で実行している場合は、ダーティ・リードと非リピータブル・リードは排除されますが、ファントム・リードは発生することになります)。良く聞くデータベース・ベンチマーク「TPC-C」は、この分離レベルの「SERIALIZABLE」が正しく動作しないと認定されないようになっています。
ご存知ない方のために、ここで分離レベルで定義されている3つの矛盾について簡単に説明します。
・Dirty Read(ダーティ・リード)
・Non-Repeatable Read(非リピータブル・リード)
・Phantom Read(ファントム・リード)
(3)Oracleデータベースの分離レベル
分離レベルは、読取みロックの保持期間によってデータに矛盾が発生することを意味しています。ただし、Oracleデータベースは競合が緩和されるように読込みロックを使用せずに読取り一貫性を行い(その時点でのコミットされてるデータをUNDOセグメントから生成して)、2つの分離レベル(READ COMMITTED(デフォルト)、SERIALIZABLE)を異なる方法で実現しています。この2つのレベルの違いは、UNDOセグメントを使用してどの時点のデータを生成するかです。「READ COMMITTED」はSQL開始時点でコミットされたデータ、「SERIALIZABLE」は非リピータブル・リードが発生しないようにトランザクション開始時点でコミットされたデータとなります(以下の例だとトランザクションBの開始時点でコミットされたデータですので、すべてAになります)。

Oracleデータベースは読込みロックを使用しないようになってはいますが、性能を重視したい場合には「READ COMMITTED」を使用して(「SERIALIZABLE」の方が多少オーバーヘッドがあるので)、運用でカバーする方が良いです。以下に「SERIALIZABLE」の実現方法と「READ COMMITTED」で発生する問題を例(座席予約システム)を使用して説明します。
- SERIALIZABLEレベル
読込みロックを使用していないので、読込み済みのデータを別のトランザクションから更新できてしまいます(この状態で何回読込んでも読取り一貫性によって非リピータブル・リードにはならないので問題ありませんが、ここで最初に読込んだトランザクションが更新すると「SERIALIZABLE」ではなくなります)。そのため、トランザクション開始時点から別のトランザクションで更新されたかのチェックをして、そのデータを更新した時点でエラーにしています(つまり、最初にアクセスしたトランザクションが更新できないとエラーにすることで「SERIALIZABLE」を実現しています)。以下の例では「【3】決めた席を更新する」ときに、その席をトランザクションBで既に更新していた場合は”ORA-8177: can’t serializable access”エラーが出力されて矛盾が防止されます。
- READ COMMITTEDレベル
これも別のトランザクションから更新できてしまいエラーにもなりません。以下の例では「【3】決めた席を更新する」ときに、その席をトランザクションBで既に更新していた場合は矛盾が発生してしまいます(ダブルブッキングになってしまいます)。これは非リピータブル・リードになりますので、このレベルでは発生してしまう訳です。
- READ COMMITTEDレベル
このダブル・ブッキングについては、以下のようにSQL文(空席のときのみ更新する)と運用で回避が可能なため、多くのシステムでは性能のためにこのように行っていると思います。このようなことを知っておくことも重要です。
■3. おわりに
今回は、第17回「チューニング」と第18回「ロックについて」と引き続いて説明しました。すべてを詳細に説明することはできませんでしたので、また機会があれば説明しようと思います。それでは、ごきげんよう。
別のトランザクションで更新されているデータ(書込みロックされたデータ)でも読込むことができるため発生する矛盾です。つまり読込みロックを獲得しないために発生します。右図の例では、2回目の問合せ前に別のトランザクションがロールバックしているため、結果が1回目と異なっています。
トランザクション内では同じ問合せを何回実行しても結果は同じになる必要がありますが(これをリピータブル・リードと呼ぶ)、トランザクション内での最初の問合せ後に他のトランザクションによって行を更新されることによって発生する矛盾です。つまりトランザクションの最後まで読込みロックを保持していないため(更新できてしまうため)に発生します。右図の例では、2回目の問合せ前にAをBに更新しているため、結果が1回目と異なっています。
トランザクション内での最初の問合せ後に他のトランザクションによって行を追加されることによって発生する矛盾です。つまり最初の問合せ時に存在しない行に対して読込みロックすることができないために発生します(存在していないデータを保護する必要があるため、これが一番難しいです)。右図の例では、2回目の問合せ前にaを挿入しているため、結果が1回目と異なっています。