システム奮闘記:その60
PostgreSQLのお勉強・その1
PostgreSQLのトランザクション、ロック機能、VACUUM、同時実行制って何?
(2007年5月6日に掲載)
2007年3月、新たにネット販売を構築する事になった。
学習に必要な項目を上げてみると、その中にはデータベースがある。
2001年秋にPostgreSQLを導入したし、SQL文も簡単な物なら書ける。
詳しくは「システム奮闘記:その7」をご覧ください。
(PostgreSQL:無償のデータベースの導入)
そして、2002年にはラージオブジェクトを使ったファイル管理も構築した。
詳しくは「システム奮闘記:その13」をご覧ください。
(PostgreSQLのラージオブジェクトを使った文書管理システム)
その程度の知識でも、ある程度の検索システムは構築できてしまう。
だが、そこから学習が止まってしまったのだ。
空白の5年間になってしまったのだ (^^;;
一応、私は日本PostgreSQLユーザー会の会員。
関西支部のお手伝いをする事があり、イベントの際、ブースに座る事があるが、
来られた方に、PostgreSQLについて聞かれても
わかんなーい (^^;;
なのだ。
お地蔵さんのように何も話さず座っているだけだ。
それでは、アカンやん!
と突っ込みを受ける状態だ。
せめて、人並にPostgreSQLの特徴、長所や短所を説明できるようになる必要がある。
というわけで、今回のネット販売の再構築の際に、データベースの勉強をして、
せめて表面的な知識だけでも身につけようと思った。
そこで取り出したのは以下の本だった。
「RDBMS 解剖学」(鈴木幸市、藤塚勤也:翔泳社)
この本を読んで行くと、いかに私がデータベースの事を全く知らないかが
思い知らされた。
今回は、トランザクション、ロック機能、VACUUMについての話を
取り上げます。
トランザクション
「RDBMS 解剖学」の7章の「トランザクションの基礎」のページを
読んでいる時だった。
トランザクションとは何か。
今まで、以下の図のような感じを思い描いていた。
今まで私の頭の中で思い描いていた
トランザクションとは(1) |
|
ある一連の処理の、開始から終了までの区間をトランザクションと呼ぶ。
もちろん、区間の開始と終了を知らせる必要はある。
|
今まで私の頭の中で思い描いていた
トランザクションとは(2) |
|
そして、その処理の間に他の処理が割り込んできて
同じデータベースのレコードに更新などの処理を行っても
割り込めないようにしている。
|
一連の処理を行っている間、別の処理が割り込んでも、割り込めないようにする。
それを「トランザクション」と思っていた。
こんな、ええ加減な知識なのだ (^^;;
いつまでも、あやふやな知識では良くないので、本を読んで勉強して、
トランザクションが何なのかを正しく知る必要がある。
勉強を始めて、しばらくして、ふと思った。そもそも・・・
「トランザクション」の意味って何?
だった。
トランザクションは「Transaction」という英語をカタカナ読みした物だ。
カタカナ語のままだったら、想像しにくいが、英語の意味で考えると
コンピューター用語は、わかりやすい事がある。
辞書で「Transaction」を引いてみた。
すると以下のような意味がある事を知る。
「Transaction」の意味 |
(事務などの)処理、(商売上の)取り引き
複数形の場合:学術報告、議事録
|
データベースでの「Transaction」の意味は次のようになっている。
アプリケーションの処理単位
データベースでも「処理」という意味で使われている。
やはり元々の言葉の意味を調べた方が、わかりやすい (^^)
さて用語の意味がわかっても、いまいちピンと来ない。
そこで本を見る。するとデータベースのトランザクションを図に表すと
以下のようになるという。
データベースのトランザクションとは? |
|
上の処理の流れは普通預金から定期預金へ10万円を移す処理だ。
この一連の流れ(処理)の事を「トランザクション」という。
|
これだと、頭の中で具体的な物が想像できる。
さて、トランザクションについて本を読み進める。
もし、トランザクションが終了しない間に、異常が発生した場合、
データを守る(整合性を保つ)ための対策が必要になる。
そのため、トランザクションが正常に終了する場合と、
異常が発生した場合の終了処理の2種類ある。
トランザクションの2つの終了処理 |
正常終了 |
トランザクションが正常に終了する場合
データベースに「COMMIT」(送る・委ねる)のコマンドを送る |
異常終了 |
異常が発生した場合、データの整合性を保つため
トランザクションが開始される前の状態に戻す
「ROLLBACK」(巻戻し)のコマンドを送る |
さて、正常終了の「COMMIT」と異常終了の「ROLLBACK」を
それぞれ図にしてみると以下のようになる。
トランザクションの正常終了 |
|
一連の処理(トランザクション)が正常であれば、
データベースに正常終了の合図である「COMMIT」を送る。
データベースは「COMMIT」の合図を受け取って、
はじめて正式にデータが更新される。
|
トランザクションの異常終了 |
|
一連の処理(トランザクション)で異常が発生した場合、
データベースに異常終了の合図である「ROLLBACK」を送る。
データベースは「ROLLBACK」の合図を受け取る事により
一連の処理を破棄して、トランザクションが開始する前の状態に
データを戻す
|
絵で見える形だと、わかりやすい (^^)
さて、正常終了の「COMMIT」と異常終了の「ROLLBACK」がどんな物か
頭の中の想像はできたが・・・
座学だけだと、オモロない! (--;;
やはり手を動かして触って、具体的にどんな振るまいをするのかを
見ておかないと「なるほど!」という感じにはならない。
「百聞は一見にしかず」なので、次の本を取り出す。
「改訂第3版 PostgreSQL完全攻略ガイド」(石井達夫:技術評論社)
2007年3月時点では改訂4版が出ているが、上の本は2001年に
PostgreSQL導入時に購入した本なので、古い版になっている。
この本は捨てられない。なぜなら・・・
石井さんのサイン入りなのらー!!
そう、プレミア付きの貴重な本なのだ (^^)
他にも以下の2冊の本を準備した。
「PostgreSQL徹底入門 8対応」
(石井達夫 監修:浅羽義之、石田朗雄、稲葉香理、永安悟史 著書:翔泳社)
「PostgreSQL完全機能リファレンス」(鈴木啓修:秀和システム)
これで準備完了。
さて、実際に、トランザクションがどんな物なのか、PostgreSQLを使ってみて
確かめる事にした。
まずは正常終了の「COMMIT」から。
トランザクションの正常終了 |
|
この時、更新処理のUPDATEより、テーブルの「b」の値は
「10」から「20」に書き換わる。
そして、正常処理の「COMMIT」の合図をデータベースに送り
正式に更新処理がされる。
|
PostgreSQLの操作では |
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> BEGIN;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 20
(1 row)
trandb=> COMMIT;
COMMIT
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 20
(1 row)
trandb=>
|
そして異常終了の「ROLLBACK」を見てみる。
トランザクションの異常終了 |
|
この時、更新処理のUPDATEより、テーブルの「b」の値は
「10」から「20」に書き換わる。
だが、異常終了の合図「ROLLBACK」を送る事によって
テーブルの値「b」は、トランザクション開始前の値に戻る
|
PostgreSQLの操作では |
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> BEGIN;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 20
(1 row)
trandb=> ROLLABACK;
ROLLBACK
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
上の操作のように、トランザクションの途中で更新処理を行っても
異常終了の「ROLLBACK」の合図を送れば、
トランザクション開始前の値に戻る事がわかる。
|
実際に、PostgreSQLを触ってみて、正常終了と異常終了の違いがわかった。
異常終了の話だが、トランザクションの開始時点まで
処理を巻き戻す事を説明しました。
だが、あとでわかった話、8.0系以降では、トランザクションの途中に
セーブポイントという印をつければ、異常終了があっても、
全て最初からやり直しではなく、セーブポイントという場所、
つまりトランザクションの途中から、やり直しができるという。
通常の「ROLLBACK」(異常終了)の場合 |
|
「ROLLBACK」を行うと、トランザクションの開始時点まで
処理が巻き戻されるため、全て最初から処理をやり直す事になる。
|
だが、8.0以降ではセーブポイントのお陰で以下のような事ができる。
セーブポイントを使った場合 (8.0以降から使える) |
|
トランザクションの途中にセーブポイントという印をつけて置く。
その後、異常が発生し「ROLLBACK」を行う際、
セーブポイントまで戻る事を指示しておけば、
巻き戻しがセーブポイントの時点のため、
最初から処理をやり直す必要はなくなる。
|
どういう事かわかったが、百聞は一見にしかずなので、
実際に手を動かして確かめる事にした。
セーブポイントを使った場合 (8.0以降から使える) |
|
トランザクションの動きの様子 |
trandb=> BEGIN;
BEGIN
trandb=> UPDATE tran1 set b = 200 ;
UPDATE 1
trandb=> SAVEPOINT sp1;
SAVEPOINT
trandb=> ROLLBACK TO SAVEPOINT sp1 ;
ROLLBACK
trandb=> SELECT * FROM tran1 ;
a | b
---+-----
1 | 200
(1 row)
trandb=>
|
セーブポイントの印をつける前に「b」の値を「200」に更新する。
ピンクの部分がセーブポイントという印をつけるSQL文。
青い部分が異常が発生したため「ROLLBACK」を行う部分。
この時、セーブポイントまで巻き戻す指示を出す。
巻き戻されたのがセーブポイントの時点なので、
「b」の値は初期の「10」ではなく「200」なのだ。
|
論より証拠で、トランザクションの処理の途中から開始する事が
可能になる事がわかった。
ところでセーブポイントは複数指定する事ができる。
つまり以下の図のように、何ヵ所かにセーブポイントを設けて
必要に応じて、巻き戻す先を指定する事ができるのだ。
複数のセーブポイントをつける事ができる (8.0以降から使える) |
|
トランザクションの途中にセーブポイントを、いくつもつける事ができる。
これにより、異常終了が発生した時に、異常終了の内容に応じて戻す位置を
指定する事ができる。
|
実際に論より証拠で、実験してみる事にする。
複数のセーブポイントをつける事ができる (8.0以降から使える) |
|
セーブポイントSP2まで巻き戻す場合 |
test1db=> BEGIN;
BEGIN
trandb=> UPDATE tran1 set b = 200 ;
UPDATE 1
trandb=> SAVEPOINT sp1;
SAVEPOINT
trandb=> UPDATE tran1 set b = 400 ;
UPDATE 1
trandb=> SAVEPOINT sp2;
SAVEPOINT
trandb=> UPDATE tran1 set b = 600 ;
UPDATE 1
trandb=> ROLLBACK TO SAVEPOINT sp2;
ROLLBACK
trandb=> SELECT * FROM tran1 ;
a | b
---+-----
1 | 400
(1 row)
trandb=>
|
セーブポイントSP1まで巻き戻す場合 |
trandb=> BEGIN;
BEGIN
trandb=> UPDATE tran1 set b = 200 ;
UPDATE 1
trandb=> SAVEPOINT sp1;
SAVEPOINT
trandb=> UPDATE tran1 set b = 400 ;
UPDATE 1
trandb=> SAVEPOINT sp2;
SAVEPOINT
trandb=> UPDATE tran1 set b = 600 ;
UPDATE 1
trandb=> ROLLBACK TO SAVEPOINT sp1;
ROLLBACK
trandb=> SELECT * FROM tran1 ;
a | b
---+-----
1 | 200
(1 row)
trandb=>
|
指定のセーブポイントまで巻き戻している事がわかる。
これにより異常終了後、やり直しが必要な部分の処理部分から
やり直す事ができる。
|
ちなみに、以下のような事もできます。
前のセーブポイントへ巻き戻し続ける事が可能 (8.0以降から使える) |
|
セーブポイントSP2に巻き戻したが、それより前のセーブポイントSP1まで
巻き戻す事が可能なのだ。
ただし、一度、SP1まで巻き戻すと、今度は、SP2へ前進させる事はできない。
|
今の所、私が扱う規模の場合、最初からやり直しでも問題はないのだが、
これが規模が大きな物になると、処理時間等の事を考慮すれば、
途中からのやり直しができるのは、PostgreSQLの凄い改良だと思う。
さて、データベースの本「RDBMS 解剖学」を読み進める。
どうやってトランザクションを保証するかの話が書かれている。
テーブルのレコード更新。
トランザクションという物を考えなければ、テーブルの値を変更する場合
そのまま上書きでも良さそうに思える。
テーブルのレコード更新を考える |
|
だが、トランザクション機能を使うと、上書きすれば良いという
単純な発想では済まないというのだ。
テーブルのレコード更新を考える |
|
トランザクションの途中で、更新した場合、正常終了の「COMMIT」と
異常終了の「ROLLBACK」の両方に備えて、更新前のレコードと
更新後のレコードの両方を持っている必要がある。
|
更新前と更新後のレコードを保管する方法には、大きく分けて2つあるという。
更新の際、新しいレコードを上書きして、更新前のレコードを
横に保管しておく方法がある。OracleやMySQLで取られている仕組みだ。
OracleやMySQLで取られている仕組み |
|
この方法でOracleやMySQLでは、トランザクション中に
更新前と更新後のレコードを取り出せるようにしている。
|
もう1つの方法は「追記型」と呼ばれる方法だ。
PostgreSQLで取られている仕組み |
|
PostgreSQLでは、更新前のレコードに印をつけておく。
そして、更新後のレコードを後ろに追加する形をとる。
追加する形態なので「追記型」と呼ぶ。
この方法でPostgreSQLでは、トランザクション中に
更新前と更新後のレコードを取り出せるようにしている。
|
追記型の場合、レコードが更新しても、削除しても、
レコードそのものが消去されるのではなく、古いレコードという印が
付けられた状態で残り続ける。
この時、初めて知った。
PostgreSQLが追記型の理由が!
そうなのです。
今までは、更新後の不要のレコードが溜っていく話は知っていましたが、
トランザクション機能が関係している事なんて、全く知らなかった。
なので、トランザクションの話と、追記型の話がつながっている事を
この日、初めて知ったのらー!! (^^)V
この程度の知識で、よく「PostgreSQLを導入している」と
平気で言っていたものだと我ながら感心してしまう (^^)
本には、PostgreSQLの場合、追記型の方がMVCCを実装しやすいと書いていた。
この時点では「MVCCって何?」だったので、軽く読み飛ばしました。
MVCCについては後述しています。
VACUUM
さて、追記型データベースの場合、問題点がある。
その問題点とVACUUMの話は密接しているので、追記型の問題点と
VACUUMについて書きます。
追記型データベースの場合、以下の問題点がある。
追記型の問題点 |
|
使わなくなった古いレコードが溜っていく。
ディスクの占有の問題が生じるだけでなく、
データベースの検索等の速度にも影響してくる。
|
そのため、不要になったレコードを消し去るための処理として
「VACUUM」が存在する。
VACUUMで不要なレコードを削除 |
|
VACUUMを行う事により、不要なレコードが削除される。
|
VACUUMが不要になったレコードを削除する事は知っていたのだが、
これがトランザクションと話とつながっていたとは、全く知らなかった。
「PostgreSQLを導入していた割には、肝心の事を理解してへんやん!」
と突っ込みを受けるが、私は、次のように言います。
だって、事務員だもーん (^^)
そう、技術者でないため、知らなくても当然なのだと居直るのだ (^^)V
さて、VACUUMの事がわかって、良かったと思っていたが、
あとで分かった話・・・
今まで書いたVACUUMの話は古かった (^^;;
今までの話は、PostgreSQL7.1までの話なのだ。
そう、もうちょっとで大恥じをかく所だった。
PostgreSQL7.1までのVACUUMの話 |
|
不要になったレコードは、放置しておくと、削除されずに溜っていく。
その不要なレコードが溜ると、ディスクの占有だけでなく、
レコードの検索等でファイル走査する場合、不要なレコードまで
走査するため、性能が悪くなる。
そこで、VACUUMを行って、不要レコードを削除していた。
|
だが、PostgreSQL7.1のVACUUMには問題があった。
VACUUM中はデータベースにロックがかかる!
個々のテーブルごとにVACUUMをかけたり、全テーブルにVACUUMを
かける事ができるが、どっちみち、該当のテーブルにロックがかかるのだ。
PostgreSQL7.1までのVACUUMの問題点 |
|
VACUUM中、データベースに接続したくても、
テーブルにロックがかかるため、接続できなくなる。
小規模なデータベースなら、1秒もかからないので、問題にならないが
大規模の場合、大量接続が考えられる上、VACUUMの作業にも
時間がかかる事から、接続できない時間帯ができてしまい、
24時間運用ができないという問題があった。
|
VACUUM中は、データベースに接続できない事から、
PostgreSQLは、24時間運用に問題があると言われてきた。
そこで、PostgreSQL7.2からは以下のようになった。
PostgreSQL7.2以降のVACUUMの話 |
|
不要になったレコードは、放置しておくと、削除されずに溜っていく。
VACUUMを行うと、不要なレコードが何かを共有メモリ上に記録する。
この時点では、不要レコードは削除されない。
さて、PostgreSQLに複数の接続があったら、それと同じだけの数の
PostgreSQLのプロセスが発生する。
共有メモリとは、その複数のPostgreSQLのプロセスが共有している
メモリ領域の事だ。
共有メモリについて詳しい事を書き出したら、長くなりますので
今回は割愛させていただきます m(--)m
|
さて、実際に「VACUUM」を実行してみる。
この場合、2通りの方法で、VACUUMを実行する事ができる。
1つ目は、SQL文でVACUUMの実行。
2つ目は、コマンドでVAUUMを実行させる方法だ。
「vacuumdb データベース名」という使い方をする。
VACUUMを実行させてみる |
SQL文でVACUUMを実行させる |
trandb=> VACUUM;
WARNING: Skipping "pg_group" --- only table or database owner can VACUUM it
WARNING: Skipping "pg_shadow" --- only table or database owner can VACUUM it
WARNING: Skipping "pg_database" --- only table or database owner can VACUUM it
VACUUM
trandb=>
|
コマンドでVACUUMを実行させる |
[suga@postgres]$ vacuumdb trandb
WARNING: Skipping "pg_group" --- only table or database owner can VACUUM it
WARNING: Skipping "pg_shadow" --- only table or database owner can VACUUM it
WARNING: Skipping "pg_database" --- only table or database owner can VACUUM it
VACUUM
[suga@postgres]$
|
共に同じVACUUMの処理を行う。
「WARINIG Skipping "pg_group"」といった感じの警告が出ている。
それは、データベースファイルの中に、ユーザーが作成したテーブル以外に
データベースを管理するテーブルもあり、それらの所有者が管理者に
なっているため、管理者所有のテーブルは、VACUUMの処理ができないと
警告しているのだ。
|
以上の事から、データベース全体をVACUUMする場合は、
管理者権限を持っているユーザーが行う必要がある。
しかし、これだけだと、不要レコードは削除されない。
一体、不要レコードを削除するには、どうすれば良いのか。
PostgreSQL7.2以降のVACUUMの話:その2 |
|
PostgreSQLは追記型データベースなので、新規レコードや
レコードの更新がある場合、テーブルに追加される形になる。
VACUUMを行うと、不要レコードの場所が記録されるため、
新規や更新するレコードは、テーブルに追加する形ではなく、
不要になったレコードの所に上書きされる。
つまり、不要なレコードの領域を再利用する形になる。
この上書きにより、レコード数が増える事がなくなるため、
ディスクを食う事もなければ、レコード検索等に時間が
かかる事もなくなる。
これだと、不要レコードの削除のために、
テーブルをロックする必要がなくなるので、
VACUUM中にデータベースに接続できなくなる問題が解消され
24時間運用が可能になるというわけだ。
|
さて、実際に、VACUUM FULLを実行してみる。
これも、SQL文で実行する方法と、コマンドを使って実行する方法がある。
以下の実行では管理者権限を持ったユーザーで行っている。
VACUUM FULLを実行させてみる |
SQL文でVACUUM FULLを実行させる |
trandb=# VACUUM FULL;
VACUUM
trandb=#
|
コマンドでVACUUMを実行させる |
[kanri@postgres]$ vacuumdb -f trandb
VACUUM
[kanri@postgres]$
|
SQL文では後ろに「FULL」をつけ、コマンドでは「-f」オプションをつける。
管理者権限を持ったユーザーで実行させたため、警告は出てこない。
|
そういえば、PostgreSQLのバージョンが上がった事で、
VACUUMによるシステム停止時間がなくなった話は聞いた事があるが、
それが、どういう事だったのか知らなかった。
まさに私は、浦島太郎なのらー!!
でも、不要なレコードが溜る問題は解消されたわけではない。
ある程度以上の規模(どんな規模か、わかりませんが・・・)になると
定期的に不要なレコードを削除する必要がある。
VACUUMに「FULL」オプションをつける。
PostgreSQL7.2以降のVACUUMの話:その3 |
|
7.2以降では、VACUUMに「FULL」オプションをつける。
すると、不要レコードが削除される。
それと同時に、不要レコードを記した共有メモリの部分も更新される。
なぜなら、VACUUMによって不要レコードが削除されるため、
不要レコードを記録した部分も、更新する必要があるからだ。
|
これで、VACUUMに関する正しい知識が身についた (^^)
同時実行制御
さて、「RDBMS 解剖学」を読み続ける。
同時実行制御の話に辿り着く。
データベースを使ったシステムは、複数のトランザクションが
走っているため、データの整合性を保ちながら、運用する必要がある。
つまり「同時に動く複数のトランザクションを制御する」話だ。
本には「ロック」の話が書いているので、それを読んでいく事にした。
データの整合性を保ちながら、トランザクションを処理する方法で
無難な方法としては、順番に処理していく方法がある。
順番にトランザクションを処理する方法 |
|
だが、上の方法だと厄介な問題が発生する。
あるトランザクションが処理を中断してしまうと、
後続のトランザクションが処理できずに、渋滞が発生する。
トランザクションの渋滞 |
|
というわけで、以下のように、同時並行で複数のトランザクションを
処理していく必要がある。
同時並行で複数のトランザクションの処理を行う |
|
複数のトランザクションを並行の処理していけば、
1つのトランザクションの動き止まってしまっても、
トランザクションの渋滞が起こる危険はなくなる。
|
だが、何の制約がない場合だと、データの整合性に問題が発生する。
複数のトランザクションの同時並行のために起こる問題 |
|
(1) |
トランザクションT1が、テーブルを読み込む。
この時、トランザクションT1は「a」の値を「100」と認識する。
|
(2) |
その次に、トランザクションT2が、「a」の値を読み込む。
この時、トランザクションT2は「a」の値を「100」と認識する。
|
(3) |
トランザクションT2は、読み込んだ「a」の値に1.05をかけた値を更新する。
この時、トランザクションT2は、「a」の値を「105」と認識する。
|
(4) |
トランザクションT1は、読み込んだ「a」の値に100を加算した値を更新する。
この時、トランザクションT1は、「a」の値を「200」と認識する。
|
ここで厄介な問題が起こる。
(3)で、トランザクションT2によって「a」の値が変更されている。
しかし、トランザクションT1は、それに気づかずに「a」の値を
「100」と認識したままで、100を加算するため、「a」の値を「200」で
更新していまう。
つまり、トランザクションT2の処理が全く反映されないのだ。
ここでデータの不整合が発生してしまうのだ。
|
思わぬ所で、データの不整合が起こってしまうのだ。
それを解消する手段の1つに「ロック機能」がある。
ロック機能を使って、先発のトランザクションから処理していく。
ロック機能をつかってデータの整合性を保つ |
|
(1) |
トランザクションT1が「a」の値を読み込む。
この時、「a」の値は「100」と認識される。
その後、データベースにロックをかける。
|
(2) |
トランザクションT2が「a」の値を読み込みにいくのだが、
ロックがかかっているため、ロックが解除されるまで待つ。
|
(3) |
トランザクションT1が読み込んだ「a」に「100」を加算した値を更新する。
この時、「a」の値は「200」になる。
そして、ロックを解除する。
|
(4) |
ロックが解除されたので、トランザクションT2は、「a」の値を読み込む。
この時、読み込んだ値は「200」なので、「a」は「200」と認識する。
|
(5) |
トランザクションT2は、読み込んだ「a」の値に、
1.05をかけた値「210」を更新する。
この時、「a」の値は210になる。
|
これだと順番にトランザクションの処理を行った形がとれるため、
全ての処理が反映され、データの整合性が保たれる。
|
上のような感じでロック機能を使って、データの整合性を保つ工夫を
行うというわけだ。
「RDBMS 解剖学」を読み進めると、データベースのロックの場合、
一般的に「二相ロック」が使われるというのだ。
ところで、二相ロックって何やねん?
本には以下のような事を書いていた。
二相ロックとは |
|
トランザクションの途中は、個々に必要なロックをかけていくのだが、
ロックを解除する事ができない。
トランザクションの終了時に、全てのロックの解除を行う。
このようなロックの事を「二相ロック」と言う
|
トランザクションの途中ではロックをかける事のみで、ロックの解除は
トランザクションの終了時のみだ。
だが、疑問が残る。
なんで「二相」と言うのやろか?
そう、言葉の意味をキチンと捉えていないと、用語の丸暗記をしても
簡単に忘れてしまうからだ。
本には書いていないので、googleなどを使ってネットで検索する。
すると、答えがあった。
「二相」と呼ぶ理由 |
|
「二相ロック」は英語の「2 Phase Lock」を翻訳した物だ。
「相」は英語の「Phase」から由来している。
個人的には「相」よりも「段階」の方がわかりやすいと思うので
「段階」という言葉で説明します。
第一段階は、トランザクションの途中の段階を意味する。
第二段階は、トランザクション終了時の段階を意味する。
ロックをかける段階と、ロックを解除する段階の2段階あるので
「二相ロック」というのだ。
|
ようやく「二相」の意味がわかったのらー!! (^^)
どうやらトランザクションの途中でかけたロックなのだが、
トランザクションを終了させるまでは解除できないようだ。
ロックが解除できないために、次の問題が起こる。
それは・・・
デッドロックなのだ!
デッドロックとは |
|
トランザクションT1がテーブルA、テーブルBの順にロックをかけ、
トランザクションT2がテーブルB、テーブルAの順にロックをかける
場合を考える。
まず、トランザクションT1がテーブルAにロックをかける。
そして、トランザクションT2がテーブルBにロックをかける。
次に、トランザクションT1がテーブルBにロックをかけようとするが
トランザクションT2がかけたロックの解除待ちになる。
同時に、トランザクションT2がテーブルAにロックをかけようとするが
トランザクションT1がかけたロックの解除待ちになる。
この時、お互いが、お互いのロック解除待ちの状態に陥り
ニッチモサッチもいかなくなる。
これがデッドロックというのだ。
|
これでデッドロックの意味がわかった。
これでロックの役目について、わかった気がした。
実際のロックのかけ方については、後述しています。
タイムスタンプ
同時実行制御だが、ロックだけでは制御するのは難しいという。
そこでロック以外の方法として、タイムスタンプを使った
同時実行制御があるという。
ところで、タイムスタンプって何?
なのだ。
本には以下のように書いていた。
タイムスタンプとは何か |
トランザクション開始時に、実行時刻(通し番号)を付け
その順番を使って、強制的に実行を制御しようとするもの
|
本には以下のような例が書かれていた。
タイムスタンプの例 |
2つのトランザクションT1、T2があるとする。
トランザクションT1が先に開始される。
その後、トランザクションT2が開始される
|
用いられるルール(1) |
トランザクションT2が更新したデータを
トランザクションT1が読み込まないようにする
|
用いられるルール(2) |
トランザクションT2が読み込んだデータを
トランザクションT1が更新しないようにする。
|
上の例とルールを図にしてみた。
ルール(1) |
|
トランザクションT2が更新したデータを
トランザクションT1が読み込まないようにする。
つまり、トランザクションT1が開始した時は、
T1はテーブルAの情報は更新されていない状態だと認識している。
だが、T1の後で開始されたトランザクションT2が
テーブルAを更新する。
そして、T1がテーブルAを読み込む場合、古い値だと認識しているので
T2が更新した値を読み込むと矛盾が生じる。
そのため、矛盾を回避するために、T1は読み込めないようにしている。
|
ルール(2) |
|
トランザクションT2が読み込んだデータを
トランザクションT1が更新しないようにする。
トランザクションT2が読み込みを行った時、
T2は更新前の状態のデータだと認識する。
その後で、トランザクションT1がテーブルAを更新すれば、
T2が読み込んだデータとの整合性が保てないために、
T1は更新できないような仕掛けになっている。
|
データの整合性を保つために、色々な工夫をしているのだなぁと
感心をする。
というより、今まで自分が、データベース管理において、
ええ加減な事をしていたのが、どんどん暴かれていくのだった (^^;;
だが、タイムスタンプの方法には問題があるという。
カスケード・アボードという現象を起こすというのだ。
カスケード・アボードとは、複数のトランザクションが動いている場合に
タイムスタンプ方式で、同時実行制御を行っていると、
読み込みの不可、書き込みの不可でトランザクションが止まってしまい、
連鎖的に他のトランザクションの動きを止めてしまう現象だという。
デッドロックに似た感じのトラブルのような気がする。
MVCC
同時実行制御で、タイムスタンプを書きました。
「RDBMS 解剖学」を読み進めると、タイムスタンプの欠点を
解消した方法として、MVCCが書いてあった。
MVCCとは「マルチバージョン同時実行制御」の意味だ。
ところで「マルチバージョン同時実行制御」って何?
言葉だけだと、何なのか、全く連想ができない。
完全日本語訳は「多版型同時実行制御」だという。
せやけど「多版型同時実行制御」って何やねん?
言葉だけでは、全く連想できない (--;;
MVCCに関する説明を読む。
MVCCは、基本的にはタイムスタンプと同じで、データの書き換えがあった際、
古いデータを消さずに残しておくというのだ。
うーん、何の事か、わからへん (--;;
考え込んだ所で、理解できる代物ではないため、前には進まない。
そんな時は、実際に、データベースを触ってみるのが一番だ。
だが、どんな時に、MVCCが働くのか、わからない。
なので、シーラカンス本を取り出し、MVCCの所を見ながら、
自分なりに思いついた方法での、2つのトランザクションの動きを見てみる
実験を行う事にした。
実験:その1 |
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
実験では、後発のT2がデータの更新した後に、先発のT1が閲覧した場合、
どういう結果が表示されるのかを確かめる事にした。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=>
|
トランザクションT2で、テーブルの「b」の値を「20」に変更した後で、
トランザクションT1で、テーブルの「b」の値を閲覧する。青い部分。
しかし、青い部分の結果は、「b」の値は「10」のままで、
トランザクションT2の更新結果が反映されていない。
|
上の実験だと、先にトランザクションT1が開始した時点では、
テーブル値は、変更前の値だと認識しているため、
後発のトランザクションT2が更新した値を読み込まずに、
更新前の値を読み込むようになっている。
この時の注意だが、2つのトランザクションとも明示的に
トランザクションの開始の「BEGIN」を行う必要がある。
もし、片方で「BEGIN」を明示的に出さない場合、以下のようになってしまう。
明示的に「BEGIN」をしない場合だと・・・ |
|
明示的にトランザクション開始の「BEGIN」を出したT1なのだが、
その後で、明示的に「BEGIN」を出していない別のトランザクションが
更新作業を行うと
|
トランザクション T1 |
別のプロセス |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 20
(1 row)
trandb=>
|
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=>
|
別のプロセスが更新する前後で、トランザクションT1が
レコードを閲覧した結果を比較すると、更新影響を受けているのがわかる。
深く調べていないので、何とも言えないが、お互いに明示的に
トランザクションの開始の「BEGIN」を出さないと、トランザクションの
同時実行制御はできないと思う。
|
では、次の実験を行う。
実験:その2 |
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
後続のT2がデータ更新した後で、先発のT1がデータ更新した場合、
どういう事が起こるのか確かめてみる事にした。
途中のSELECT文は、テーブルの値の変化を見るための物です。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> UPDATE tran1 SET b = 30 ;
(トランザクションT2が終了するまで待ち続ける)
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=>
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 20
(1 row)
trandb=>
|
後続のトランザクションT2がデータ更新した後に、先発のトランザクションT1が
データ更新を行おうとするが、トランザクションT2の終了待ち状態になる。
|
次に以下の実験を行った。
実験:その3 |
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
T2がテーブルのデータを消去した後で、T1がデータを更新する。
そして、2つのトランザクションを「COMMIT」で終了させる。
テーブルのデータはどうなっているのか確かめてみる。
先発のT1は開始時に、テーブルのデータが存在していると認識しているので、
その更新データが反映された形になるのか、それともT2が
データを消去したため、T1が何をしても、データは存在しない形になるのか
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 30 ;
trandb=> COMMIT;
COMMIT
trandb=> SELECT * FROM tran1 ;
a | b
---+----
(0 rows)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> DELETE FROM tran1 ;
DELETE 1
trandb=> COMMIT;
COMMIT
trandb=>
|
トランザクションT2でデータを消去(赤い部分)
その後で、トランザクションT1でデータを更新(青い部分)
両方のトランザクションを「COMMIT」で終了させて、
テーブルの中身を見てみる(緑の部分)
結果は、データが消去されていた。トランザクションT2の結果が反映され
トランザクションT1の更新は全く反映されていなかった。
|
片方のトランザクションでデータを消去したつもりで、
もう一方のトランザクションは、データの更新したつもりが、
結果は、データ消去された形になる。
これって問題ではないだろうか。
データベースの運用の課題になる部分だ。
さて、次の実験を行う。
実験:その4 |
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
トランザクションT2がデータ更新を行った後、
トランザクションT1がデータの中身を閲覧する。
そして、T2を「COMMIT」で終了させた後、T1がデータの閲覧を行う。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 30
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 30 ;
UPDATE 1
trandb=> COMMIT;
COMMIT
trandb=>
|
トランザクションT2でデータを更新させた後、トランザクションT1で
データを閲覧した場合、データの変更前の値が出てきた。
その後、T2を「COMMIT」で終了させた後、T1でデータを閲覧すると
更新が反映された後の値が出てくる。
|
なんとなく問題なさそうに思えるが、実は、問題が発生しているのだ。
まだ、トランザクションT1が終了していない間の値が更新され、
その値が表示される。
そのため、以下の実験結果のように、予想外の結果になる事がある。
実験:その5 |
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
トランザクションT2が「b」の値を「20」に更新を行った後、
トランザクションT1がデータの中身を閲覧する。
そして、T2は「COMMIT」で終了させる。
T1は「b = b + 10」という形でデータの更新を行い、
再度、T1は「b」の値を読み込む。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> UPDATE tran1 SET b = b + 10 ;
UPDATE 1
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 30
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
先にトランザクションT1を開始して、その後で、トランザクションT2を開始する。
トランザクションT2が「b」の値を「20」に更新を行った後、
トランザクションT1がデータの中身を閲覧する。
この時、トランザクションT1はT2の更新前の値を読む込むため、
T1は古い値「b = 10」と認識する。
そして、T2を「COMMIT」で終了させる。
この時、T2が「b = 20」にした値がデータに反映される。
だが、そんな事を知らないT1は、T1は「b = 10」と認識した状態で
「b = b + 10」とする。
T1としては「b = 20」に更新されると思うのだが、結果は、
「b」の値が「30」と表示される。
明らかに、データに矛盾が生じる問題が発生するのだ!
|
MVCCとロックを上手に組み合わせる必要がある!
という事がわかった。
そして、MVCCの説明にあった「データの書き換えがあった際、
古いデータを消さずに残しておく」という事も、実験でなんとなくわかった。
だが、「なんとなく」だと、ハッキリわかった事にはならない。
なので、このままでは・・・
MVCCが何なのか、人に説明できへん!! (^^;;
そして、ハッキリわかっていないと、データベースを活用した
システムを構築する場合に、キチンとした物ができない事になる。
それではアカンやん!
という事で、MVCCとは何かを調べてみる。
最初の頃は、何を見ても、いまいちピンと来なかったのだが、
色々、調べているうちに、だんだんMVCCが何なのかが見えて、
だいぶ後になって、どういう事なのかが、ようやくわかった。
MVCCというのは、以下の仕組みの事だった。
テーブルのレコードの古い新しいを区別するために、レコードには
そのレコードを作成したトランザクションID(XID)が記録される。
MVCCとは何か (XID:トランザクションIDについて) |
|
各レコードには、そのレコードを作成・更新したトランザクションのID
即ち、XIDが記録される。
それによって、どのトランザクションが作成・更新したかがわかる上、
現在、動いているトランザクションのXIDと比較して、
レコードを作成したトランザクションが、自分よりも先か後かがわかる。
ちなみに、OIDとは、オブジェクトIDの意味で、レコード別に
固有のIDが割り当てられる。
|
レコードを新規に書き込んだり、更新したトランザクションのID(XID)が
レコードに記録される事がわかった。
レコードを更新した場合、どういう事になるのか。
MVCCとは何か (レコード更新を行った場合) |
|
XIDが4300のトランザクションが、りんごの価格を300円に値上げするため、
りんごのレコードを更新する。
PostgreSQLは追記型なので、テーブルの後ろに更新レコードを追加する形になる。
この時、OIDは、りんごのレコードなので、更新する前のOIDと同じ物を使うが
XIDは、更新したトランザクションのIDを書き込むため、4300が入る。
まだ、トランザクションの途中で、終了の「COMMIT」を出していないため、
不要レコードの部分には印は入らない。
なぜなら、「COMMIT」を出して、初めて、更新したレコードの値を
確定するわけであって、この時点では確定したわけでないからだ。
この状態だと、更新前のレコードも、更新後のレコードも取り出せる。
|
これで、同じレコード(OID)でも、XIDの値の違いで、更新前と更新後の
レコードを共存させる事ができるのが分かった。
まだ、トランザクションの終了の「COMMIT」を出していないため、
更新後のレコードを確定させていないため、更新前のレコードの部分には
まだ、不要の印の「不要フラグ」が立てない。
更新前と更新後の両方の値が取り出せるため、以下のような事が可能になる。
MVCCとは何か (更新前と更新後を区別できる) |
|
2つのトランザクションが開始される。
後続のトランザクション(XID:4300)が、りんごの値上げをするため
レコードの更新を行う。
この時点ではトランザクションの終了の「COMMIT」を出していないため、
更新前と更新後の両方のレコードが取り出せる。
先発のトランザクション(XID:4250)は、トランザクション開始前の
レコードを読み込むのが前提なので、レコードに記録されているXIDの値と
自分自身のXIDとを比較して、古い方(更新前)のレコードを読み込む。
これによってデータの整合性を保てる仕掛けになっている。
|
後発のトランザクションがレコードを更新しても、先発のトランザクションが
古い値を読み込める仕掛けがわかった。
つまり、この仕掛けの事を
MVCCと言うのらー!
MVCCの仕掛けがわかると、先に行った実験:その1を行った際、
なぜ、その結果になったのかが理解できる。
もう一度、実験:その1を見てみる。
実験:その1 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=>
|
後発のトランザクションT2がレコードを更新しても、
「COMMIT」をしていないため、更新したレコードは、未確定の状態で、
更新前のレコードも生きている状態だ。
先発のトランザクションT1は、自分自身が開始された時のレコードを
データの整合性を保つため、読み込む前提になっている。
この時点では、トランザクションT2がレコードを更新しているが、
更新前のレコードも生きているので、更新前のレコードを読み込む。
そのため、更新前の値が取り出せるのだ。
|
MVCCの仕掛けがわかれば、実験の結果にも納得できる (^^)
ところで「追記型データベースの場合、MVCCの実装しやすい」と
前述しました。
つまり、同じテーブル上の更新前と更新後のレコードを共存させるため、
レコードを取り出しやすいという利点があり、その利点を使っているのだ。
MVCCの仕掛けを理解したので
ようやく納得できた (^^)
ところで、MVCCは「Multi Version Concurrency Control」の略なので、
日本語訳として「マルチバージョン同時実行制御」と言ったり、
完全日本語訳として「多版型同時実行制御」と言ったりする。
翻訳の難しさを考える |
「Multi Version Concurrency Control」の完全日本語訳は
「多版型同時実行制御」だが、これでは意味がわからない。
そこで英英辞典を取り出して調べてみた。
「Version」の訳し方次第で意味がわかりやすくなるのか、
それともわかりにくくなるのかの分かれ目になる気がした。
「Multi」は「多数」の意味なので「多」という訳になる。
「Concurrency」は「同時に発生している2つ以上の出来事」という意味だ。
「Control」は「制御」の意味だ。
さて、問題は「Version」だ。
「版」と訳しているのは「発行された個々のトランザクション」の
意味合いだと思われる。
だが、「版」だと、普段使わない言葉だけに、わかりづらい。
しかし、他に良い訳がないので、「版」とならざる得ない気がする。
補足をいれた長い翻訳文にすると「複数のトランザクションを
同時に実行させるための制御方法」という文になるのだが、
こんな物は用語としては使えない。
なので、外国で生まれた用語を上手に短く訳す難しさを感じる今日この頃。
|
さて、MVCCが何かがわかった。
今までの説明だと、トランザクション終了の「COMMIT」を出す前の話で
「COMMIT」を出した後の話を書きます。
MVCCとは何か (「COMMIT」を出した場合) |
|
トランザクションは「COMMIT」を出す事によって、
更新したレコードの値を確定させる。
この時、初めて、更新前のレコードに「不要フラグ」に印をつける。
こうなると、更新前のレコードが取り出せなくなる。
|
「COMMIT」を出してしまうと、更新前のレコードの不要フラグが立ち
取り出せなくなる。
なので、以下の現象が起こってしまう。
MVCCとは何か (「COMMIT」の影響) |
|
2つのトランザクションが動いていて、後続のトランザクション(XID:4300)が
りんごの値上げのために、レコードの更新を行う。
この時は「COMMIT」を出していないため、更新前のレコードも取り出せる。
なので、先発のトランザクション(XID:4250)は、更新前のレコードを読み出せる。
しかし、後続のトランザクション(XID:4300)が「COMMIT」をしてしまうと
更新前のレコードに不要フラグが立ち、更新前のレコードが取り出せなくなる。
そして、先発のトランザクション(XID:4250)が、もう一度、レコードを
読み込む際には、更新後のレコードを読み込んでしまうため、
先発のトランザクションは、何もしていないにも関わらず、
全く違う値を読み込んでしまう事態が起こってしまう。
|
いくら先発のトランザクションが、自分自身が開始した時の
レコードの値を読みたくても、後発のトランザクションがレコードを更新し
しかも「COMMIT」で更新レコードを確定してしまうと、
更新前のレコードが読めなくなるのだ。
さて、先にMVCCの動作実験として「実験:その5」を行った。
ようやく、ここに来て、その結果が出る理由が理解できた。
そこで、もう一度、実験:その5を見てみる事にした。
実験:その5 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> UPDATE tran1 SET b = b + 10 ;
UPDATE 1
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 30
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
UPDATE 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
後発のトランザクションT2がレコードを更新しても、
「COMMIT」していない場合、更新レコードは未確定なため、
更新前と更新後のレコードが共存している。
そのため、先発のトランザクションT1が、レコードを読み込んでも
問題なく更新前のレコードを読み込める。
だが、後続のトランザクションT2が「COMMIT」をしてしまうと、
更新レコードが確定してしまうため、更新前のレコードが取り出せなくなる。
そのため、先発のトランザクションT1がレコードを読み込むと
更新後のレコードの値を読む事態になる。
トランザクションT1は何もしていないのに、全く違い値を読むため
データの整合性の問題が生じるのだ。
|
これで納得できた (^^)
この事から、MVCCではデータの整合性が不十分だと言える。
そのため、本などには「ロック」と組み合わせる必要があると書いているが、
その理由も納得できる。
この時点では知らなかった話だが、トランザクションの隔離レベルの
変更を行うという手段もある。隔離レベルについては後述しています。
ロック機能
この同時実行制御で「ロック」が重要になる。
ロックの勉強をする事になった。
PostgreSQLを導入して、5年半になるが、正直な事を書くと・・・
ロックなんて使った事ありませーん (^^)
なのだ。
だが、開き直っている私は・・・
事務員がロックを使う高度なシステムなんて構築できるわけないのらー!
毎度の如く、スゲー言い訳した所で、ロックの話を続けます。
複数のトランザクションを扱う場合、順番にトランザクションを処理すれば
データの整合性に問題が起こらないが、そうは問屋が卸さない。
1つのトランザクションが処理に時間がかかり、長時間止まってしまうと
他のトランザクションが処理できない問題が生じる。
そのため同時並行に複数のトランザクションを処理する必要がある。
でも、MVCCでは同時実行制御を行うには不十分なので、
ロックとの組み合わせが必要になるという。
ところで、ロックには2種類ある。
ロックの種類 |
共有ロック |
このロックをかけると、他のトランザクションは
書き込みはできないが、読み込みはできる。
読み込みは、どのトランザクションも可能なので
「共有ロック」という。
|
排他ロック |
このロックをかけると、他のトランザクションは
読み書き不可になる。
読み書きできない排他的な状態なので「排他ロック」という。
|
ロックを行う場合、ロックをかける単位が2通りある。
「行ロック」と「テーブルロック」だ。
ロックの単位 |
行ロック |
テーブルの特定の行をロックする |
テーブルロック |
テーブル全体にロックをかける |
さて、まずは特定の行にロックをかける行ロックが、
どんな物か見てみる。
行ロックでも、最初は他のトランザクションが閲覧可能な
共有ロックにしてみた。
行ロック(共有ロックをかけてみた) |
|
先発のトランザクションT1が開始したと同時に、レコードに
共有ロックをかける。
そして、後発のトランザクションが開始を明示的に表示する
「BEGIN」を出す前と後で、SELECTを行い、レコードが読めるかどうかを
見てみる事にした。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 FOR UPDATE ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
先発のトランザクションT1で、行ロックをかけたのだが、
共有ロックのため、後発のトランザクションT2では
問題なく、テーブルを見にいく事ができる。
|
さて、共有ロックの場合、他のトランザクションは閲覧可能だ。
次に、共有ロックをかけた状態で、他のトランザクションが
更新処理する場合を見てみる事にした。
行ロック(共有ロックをかけてみた) |
|
トランザクションT1でレコードに共有ロックをかけてみる。
その後で、トランザクションT2が共有ロックがかかったレコードに
更新をかけてみると、どうなるのか見てみる事にした。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 FOR UPDATE;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 ;
(T1が終了するまで待ち状態が続く)
|
いくらトランザクションT2がレコードを更新したくても
トランザクションT1がかけたロックのお陰で更新できない状態になる。
トランザクションT1の終了(COMMITかROLLBACK)をしない限り、
ロックが解除されないため、T2は待ちの状態が続く。
|
上の実験では、テーブルが1レコードしかないため、見た目は
テーブルロックに近い形になっている。
そこで、2レコードのテーブルにしてみた。
まずは、1レコード目だけ行ロックをかけて、他のトランザクションが
2レコード目の更新作業を行う実験をしてみた。
行ロックがかかっていないレコードを更新してみる
(共有ロック) |
|
トランザクションT1で、「a」の値が「2」に該当するレコードに
行ロックをかけてみた。
その次に、トランザクションT2で、ロックがかかっていないレコードの
更新を行ってみた。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 WHERE a = 2 FOR UPDATE;
a | b
---+-----
2 | 100
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 WHERE a = 1;
UPDATE 1
trandb=>
|
行ロックがかかっていないレコードは、問題なく更新できるのがわかる。
|
では、行ロックをかけているレコードを更新した場合を見てみる事にした。
行ロックがかかっているレコードを更新してみる
(共有ロック)
|
|
トランザクションT1は「a」の値が「2」に該当するレコードに行ロックをかける。
そして、別のトランザクションT2は、「a」の値が「2」に該当するレコードの
更新を行ってみる。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 WHERE a = 2 FOR UPDATE;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> UPDATE tran1 SET b = 20 WHERE a = 2 ;
(T1が終了するまで待ち状態が続く)
|
トランザクションT2が、行ロックがかかっているレコードを更新したくても
更新できずに、待ち状態になっている。
トランザクションT1の終了(COMMITかROLLBACK)まで、ロックが解除は解除されず
更新は待たされるのだ。
|
これで行ロックが、どんな物かがわかった。
頭で理解するだけでなく、手を動かす必要があるなぁと思った (^^)
次は、テーブル全体のロックをかけるテーブルロックを行ってみた。
まずは、ロックでも共有ロックをかける実験から行った。
テーブルロック(共有ロックをかけてみた) |
|
トランザクションT1でテーブルロックをかけてみた。
そして、トランザクションT2でテーブルを閲覧できるかどうかを
確かめて見る事にした。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> LOCK TABLE tran1 IN SHARE MODE;
LOCK TABLE
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+-----
1 | 10
2 | 100
(2 rows)
trandb=>
|
トランザクションT1で、テーブルロック(共有ロック)をかけたが
共有ロックのため、別のトランザクションT2が閲覧しても
問題なく閲覧は可能だとわかる。
ちなみに、この時、トランザクションT2が更新を行う場合は、
トランザクションT1が終了するまで、待ち状態になる。
|
共有ロックだと、他のトランザクションがロックをかけたテーブルや
レコードを閲覧する事ができる。
次に、排他ロックをかけてみる。
テーブルロック(排他ロックをかけてみた) |
|
トランザクションT1で、テーブルロック(排他ロック)をかけてみる。
そして、別のトランザクションT2で、テーブルのレコードの閲覧を行ってみる。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> LOCK TABLE tran1 IN ACCESS EXCLUSIVE MODE ;
LOCK TABLE
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
(T1が終了するまで待ち状態が続く)
|
排他ロックのため、別のトランザクションT2が閲覧したくても
T1の終了(COMMITかROLLBACK)を待つ状態になっている。
|
予想通り、排他ロックがかかったテーブルを閲覧したくても、
ロックをかけたトランザクションが終了しない限り、閲覧できない事がわかった。
以上の実験から、ロックがどんな物かがわかった。
やはり手を動かして、自分の目で見ないと、本のお勉強だけでは身につかない。
行ロックとテーブルロックの違いは、特定の行にロックをかけるのか
それともテーブル丸ごとロックをかけるのかの違いだ。
だが、この違いは大きい。
行ロックの場合、以下の利点がある。
行ロックの利点 |
|
複数のトランザクションが同じテーブルのレコードを見にいく場合、
個々の行にロックをかける場合だと、同じ行を見にいかない限り
衝突する事がないため、ロック解除待ちが起こらず、
同時に複数のトランザクションが円滑に処理できる。
|
だが、テーブルロックの場合だと、以下の問題が出てくる。
まずはテーブルロックでも、共有ロックから。
テーブルロックの問題点(共有ロックの場合) |
|
あるトランザクションがテーブルロック(共有ロック)をかけたら
閲覧以外の処理を行うトランザクションは、ロック解除待ちの状態になり
処理ができなくなる。
|
まだ、共有ロックはマシなのだが、排他ロックになると次のようになる。
テーブルロックの問題点(排他ロックの場合) |
|
あるトランザクションがテーブルロック(排他ロック)をかけたら
他の全てのトランザクションは、ロック解除待ちの状態になり
全く処理ができなくなる。
|
そういうわけで、行ロックが便利な事がわかる。
複数のトランザクションを同時に実行させ、データベースのデータの
整合性を保つには、MVCCでは
しかも、ロックは「二相ロック」なので、トランザクションが終了しない限り
ロックは解除されない。
だが、ロックを使う場合には、デッドロックには注意が必要だ。
デッドロックを起こしてみる事にした。
デッドロックを起こしてみる |
デッドロックとは (おさらい) |
|
トランザクションT1がテーブルA、テーブルBの順にロックをかけ、
トランザクションT2がテーブルB、テーブルAの順にロックをかける
場合を考える。
まず、トランザクションT1がテーブルAにロックをかける。
そして、トランザクションT2がテーブルBにロックをかける。
次に、トランザクションT1がテーブルBにロックをかけようとするが
トランザクションT2がかけたロックの解除待ちになる。
同時に、トランザクションT2がテーブルAにロックをかけようとするが
トランザクションT1がかけたロックの解除待ちになる。
この時、お互いが、お互いのロック解除待ちの状態に陥り
ニッチモサッチもいかなくなる。
これがデッドロックというのだ。
|
デッドロックが発生している様子 |
トランザクションT1 |
trandb=> BEGIN;
BEGIN
trandb=> LOCK TABLE tableA IN SHARE ROW EXCLUSIVE MODE ;
LOCK TABLE
trandb=> LOCK TABLE tableB IN SHARE ROW EXCLUSIVE MODE ;
LOCK TABLE
trandb=>
|
トランザクションT2 |
trandb=> BEGIN ;
BEGIN
trandb=> LOCK TABLE tableB IN SHARE ROW EXCLUSIVE MODE ;
LOCK TABLE
trandb=> LOCK TABLE tableA IN SHARE ROW EXCLUSIVE MODE ;
ERROR: deadlock detected
DETAIL: Process 7005 waits for ShareRowExclusiveLock
on relation 25123 of database 25122; blocked by process 7003.
Process 7003 waits for ShareRowExclusiveLock
on relation 25127 of database 25122; blocked by process 7005.
trandb=>
|
PostgreSQLにはデッドロックを検出する仕組みがあるため、
赤い部分で示しているように、デッドロックが発生したら
エラーがでるようになっている。
エラーを検出した側のトランザクションは異常終了の形をとって
トランザクションを終了させる。
もし、デッドロックを検出する仕組みがなければ、
お互いロック解除待ち状態に陥り、ニッチモサッチモいかなくなる。
|
この実験を行って、PostgreSQLにデッドロック検出機能が付いていたのを
初めて知った (^^)
デッドロックの検出だが、ロックがかけてから、どのくらいの時間が経過したら
デッドロック検出を行うかの設定ができる。
デッドロックの設定 (postgresql.conf) |
7.3系の場合 |
#
# Misc
#
#autocommit = true
#dynamic_library_path = '$libdir'
#search_path = '$user,public'
#datestyle = 'iso, us'
#timezone = unknown # actually, defaults to TZ environment setting
#australian_timezones = false
#client_encoding = sql_ascii # actually, defaults to database encoding
#authentication_timeout = 60 # 1-600, in seconds
#deadlock_timeout = 1000 # in milliseconds
#default_transaction_isolation = 'read committed'
#max_expr_depth = 10000 # min 10
#max_files_per_process = 1000 # min 25
#password_encryption = true
#sql_inheritance = true
#transform_null_equals = false
#statement_timeout = 0 # 0 is disabled, in milliseconds
#db_user_namespace = false
|
8.2系の場合 |
#---------------------------------------------------------------------------
# LOCK MANAGEMENT
#---------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64 # min 10
# (change requires restart)
# Note: each lock table slot uses ~270 bytes of shared memory, and there are
# max_locks_per_transaction * (max_connections + max_prepared_transactions)
# lock table slots.
|
7.3系と8.2系では、設定ファイル(postgresql.conf)の記述は異なる。
両方とも赤い部分が設定箇所だ。
初期状態では1秒(1000ミリ秒)になっている。
つまり、初期状態ではロックがかかってから、1秒後には
デッドロックが発生しているかどうかを調べるのだ。
|
この設定だが、あまり短くすると、ロックをかける度に、頻繁に
デッドロック検出が行われるため、データベースの性能低下を招いてしまう。
中小企業だと、1秒間に多数の接続が起こる事態は考えにくいが
大手の基幹システムや、人気コンサートのチケットのネット販売では
恐ろしい瞬間風速が吹くため、この辺りの設定はキチンと行わないと
大変になる事が予想できるのが感じられる。
隔離レベル
トランザクションの同時実行制御で、MVCCやロックの話を書きましたが、
トランザクションの隔離レベルの話も書く事にします。
これはトランザクションの機能で、複数のトランザクション同士、
お互いの処理の邪魔をしないようにする物だ。
お互いが影響しあう度合を、SQL92では4段階に分けている。
SQL92って何やったっけ?
SQL92を調べてみると、1992年に発表された国際標準化SQL言語の事だ。
ちなみに、1999年に発表された国際標準のSQL言語の事をSQL99という。
つまり「92」とは「92年版」で、「99」とは「99年版」という意味だ。
さて、トランザクションがお互いに影響し合う度合を表す名称と、
どの影響については以下の通りだ。
トランザクションの隔離レベル |
隔離 度合 |
隔離レベル名 |
ダーティー リード |
反復不能 読み取り |
ファントム リード |
弱 |
READ UNCOMMITED |
可能性有 |
可能性有 |
可能性有 |
↓ |
READ COMMITED |
可能性有 |
可能性有 |
安全 |
↓ |
REPEATABLE READ |
可能性有 |
安全 |
安全 |
強 |
SERIALIZABLE |
安全 |
安全 |
安全 |
だが、こんな表を見ても・・・
ダーティーリードって何やねん (^^;;
なのだ。
もちろん「反復読み込み不能」も「ファントムリード」も何なのかが
わからないため、表を見ても、どういう事なのかが、わからない。
そこで、まずはダーティーリードから調べる事にした。
英語の綴りは「Dirty Read」なので、直訳すると「汚れ読み取り」だ。
うーん、イマイチだ・・・。
ダーティーリード(Dirty Read)とは、以下のような場合の事を
表す事がわかった。
ダーティーリード(Dirty Read) |
|
後発のトランザクションT2がテーブルの更新を行った後、
「COMMIT」をしない状態であっても、先発のトランザクションT1は
更新した値を読んでしまう現象の事を言う。
ダーティーリードが起こる「UNCOMMITTED READ」の状態だが、
「UNCOMMITTED READ」を直訳すると「未終了状態での読み込み」で
つまりテーブルを更新したトランザクションが「COMMIT」で
終了をしていなくても、他のトランザクションが更新した値を
読み込んでしまうという事だ。
|
つまりトランザクションの隔離レベルが一番低い状態だと
トランザクションがお互いの処理の影響を簡単に受けてしまうのだ。
さて、実際に、ダーティーリード(Dirty Read)を見てみる前に
トランザクションの隔離レベルの設定方法から
トランザクションの隔離レベルを変更するSQL文 |
trandb=> SET TRANSACTION ISOLATION LEVEL 隔離レベル;
SET
trandb=>
|
ここで注意が必要なのは隔離レベルの指定を行う際に、
「READ UNCOMMITTED」の場合は、「READ UNCOMMITTED」ではなく
「UNCOMMITTED READ」と指定するのだ。順序が逆になっている。
同様に「READ COMMITED」の場合も、「READ COMMITED」ではなく
「COMMITED READ」と指定する。
この事を知らなかっただけに、エラーが出ても何故かわからず
考え込んでしまったのだ (^^;;
あとの「REPEATABLE READ」と「SERIALIZABLE」は、
そのまま指定すれば問題はない。
|
さて、ダーティーリード(Dirty Read)が起こる様子を
見てみる事にしてみた。
ダーティーリード(Dirty Read)を起こしてみる |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;
SET
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;
SET
trandb=> UPDATE tran1 set b = 200 ;
UPDATE 1
trandb=>
|
トランザクションT1の部分で、テーブルの値(赤い部分)を見てみる。
本来なら、ダーティーリードの場合、トランザクションT2が行った
更新の影響(青い部分)で、「b」の値は「200」なのだが、
実験結果は、どういうわけか「10」のままなのだ
|
なんでダーティーリードが起こらへんねん (TT)
トランザクション隔離レベルの設定で、ダーティリードが起こるように
「UNCOMMITTED READ」にしているのだが謎だ・・・ (--;;
なぜかを調べてみたら、PostgreSQLでは「UNCOMMITTED READ」の隔離レベルは
サポートしていないのだ。
つまり、「UNCOMMITTED READ」の隔離レベルの状態にする事はできない。
だが、「UNCOMMITTED READ」の隔離レベルの設定した際は
全くエラーが出なかった。謎だ・・・。
なので、この時は・・・
事務員なので、わかりませーん (^^)
と得意の常套手段で逃げる事にした。
だが、あとでPostgreSQLのマニュアルを見てわかった話だが、
「UNCOMMITTED READ」の設定しても、次のレベルの「READ COMMITED」に
なってしまう事がわかった。
なので、PostgreSQLでは絶対にダーティーリードは起こらないと断言できる!
さて、次のレベルの「READ COMMITED」のレベルについて説明します。
このレベルはPostgreSQLだけでなく、Oracle、MySQLなど
多くのデータベースにおいて、初期設定状態での
トランザクション隔離レベルになっているという。
PostgreSQLにおけるトランザクションの 隔離レベルの初期状態を見てみる
|
trandb=> SHOW default_transaction_isolation;
default_transaction_isolation
-------------------------------
read committed
(1 row)
trandb=>
|
さて、隔離レベル「READ COMMITED」がどういう状態なのか
見てみる事にしてみた。
隔離レベル「READ COMMITED」について |
|
後発のトランザクションT2が更新を行っても、「COMMIT」をしない限り、
先発のトランザクションT1は、更新前の値を読み込む。
この図は、MVCCの部分でも出てきました。
そう、後発のトランザクションがデータを更新しても
「COMMIT」していない限り、先発のトランザクションは
更新前のデータを読み込む仕掛けになっているのだ。
つまり、MVCCの実装のお陰で、隔離レベル「READ COMMITED」が
確保できているのだ。
|
ここでトランザクション隔離レベルとMVCCが結び付いた (^^)
だが、この状態でも問題が発生する。
「反復不能読み取り」という現象で、以下のような事だ。
「反復不能読み取り」とは何か |
|
これもMVCCの所で出てきました話と同じです。
後発のトランザクションT2が更新作業を行った後、
「COMMIT」で終了してしまうと、先発のトランザクションT1は
更新後のデータを読み込んでしまう現象なのだ。
これを「反復不能読み取り」という。
「反復不能」は、トランザクションT1は何もしていないのに、
トランザクションT2が更新して「COMMIT」する前と後では
読み込む値が同じでない、つまり反復読み込みできない状態の事を
意味するのだ。
トランザクションの隔離レベルが「READ COMMITED」なのは
「COMMIT」してしまった物を読み込んでしまうという意味なのだ。
|
PostgreSQLでは、初期状態では「反復不能読み取り」が起こってしまう。
なので、「反復不能読み取り」を防ぐためには、もう一つ上の
隔離レベルの「」に設定する必要がある。
そこでトランザクションの隔離レベルの「REPEATABLE READ」に設定して、
「反復不能読み取り」が起こるか起こらないのか確かめる事にした。
隔離レベルの「REPEATABLE READ」の場合 |
|
トランザクションの隔離レベルを「REPEATABLE READ」に設定すれば
上のように後発のトランザクションT2が更新を行い「COMMIT」しても
先発のトランザクションは更新前のデータを読み込むはず。
なので、実験を行ってみた。
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
SET
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
SET
trandb=> UPDATE tran1 set b = 200 ;
UPDATE 1
trandb=> COMMIT;
COMMIT
trandb=>
|
実験の結果、後発のトランザクションT2が更新を行い、
「COMMIT」をしても、先発のトランザクションT1は影響を受けずに
更新前のデータを読み込んだ。
|
うまくいったので、喜んだのだが、喜びも束の間だった。
それはPostgreSQLでは隔離レベル「REPEATABLE READ」は
サポートしていない事を知ったからだった (^^;;
なので・・・
なんで実験が成功したんか、わからへん (--;;
だった。
設定の際にエラーが出ていないだけに謎なのだ。
あとでPostgreSQLのマニュアルを見てわかった話なのだが、
隔離レベルの設定で「REPEATABLE READ」を選んでも、それより一つ上の
隔離レベルの「SERIALIZABLE」になる事がわかった。
なので実験が成功したのだった。結果良しとしよう! (^^)
さて、隔離レベル「REPEATABLE READ」は、PostgreSQLではサポートしていない。
ところで、もし隔離レベル「REPEATABLE READ」が実現した場合でも、
まだ問題がある。
それはファントムリードが起こるというのだ。
ところでファントムリードって何?
そこで「ファントム」の英語の綴りを調べてみる事にした。
最初「fantom」かなぁと思って辞書を調べるが載っていない。
全く見当がつかないので、PostgreSQLの本家のホームページにある
英語のマニュアルを見て綴りを調べると、わかった。
Phantomだった!
「f」ではなく「ph」だった。そういえば「ph」の綴りで
「f」の発音をする場合が英語にはあるのを思い出す。
二相ロックの「相」の「Phase」(フェイズ)が良い例だ。
こんなややこしい綴りをする英語は嫌いじゃ!!
英語嫌いの私なので「イザナギ、イザナミがお産みになった神国・日本に
異国の言葉を持ち込むな」と叫びたくなるのだが、こんな事を言うと
「漢字は何やねん」と突っ込まれるので、この辺でやめておきます (^^)
さて「phantom」の意味を調べると・・・
うらめしや〜。角はパン屋〜、表はうどん屋〜
つまりお化けなのだ。幽霊、幻影、錯覚の意味がある。
さて、どんな現象かと言うと以下の通りです。
ファントムリード(Phantom READ)とは何か No1 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+---
(0 rows)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> DELETE FROM tran1 ;
DELETE 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
最初、先発のトランザクションT1がデータを閲覧する際は問題がない。
レコード数(赤い部分)だが問題なく「1」になっている。
だが、後続のトランザクションT2がデータ消去を行い「COMMIT」をした後で
もう一度、トランザクションT1がデータを閲覧した時、
レコード数(赤い部分)が「0」になっている。データが読めなくなる現象だ。
トランザクションT1自身は、何もしていないのに、見えていたはずの物が
見えなくなる現象なので、ファントムリードという。
|
見えていたはずの物が見えなくなる現象もあれば、全く反対に
今までなかった物が、急に見える例もある。
以下の場合も、ファントムリードと呼ぶ
ファントムリード(Phantom READ)とは何か No2 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+-----
1 | 10
2 | 100
(2 rows)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> INSERT INTO tran1 VALUES ( 2 , 100 );
INSERT 733645 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
最初、先発のトランザクションT1がデータを閲覧する際は問題がない。
赤い部分のレコード数が「1」なので問題はない。
だが、後続のトランザクションT2がレコードの追加を行い
「COMMIT」をした後で、もう一度、トランザクションT1がデータを閲覧した時、
追加したレコードも見えてしまう現象だ。
追加されたレコードの明細(青い部分)と、レコード数(赤い部分)が
「2」になっている。
トランザクションT1自身は、何もしていないのに、いつの間に
レコードが増えている現象の事も、ファントムリードという。
|
ファントムリード(Phantom read)の命名だが、見えているはずの物が
見えなくなったり、反対に、ないはずの物が、出てきたりするため、
お化けに例えて「ファントムリード」(お化けデータを読む意味)
と呼ぶようになったという。
まさに・・・
うらめしや〜。角はパン屋〜、表はうどん屋〜
「山田く〜ん、座蒲団三枚取り上げ」のギャグだなぁ (^^;;
さて、このお化けを退治するには、霊媒師を呼んで除霊を行う必要はなく
御札を張ってお化けを動きを止めれば良いのだ。
その御札とはトランザクションの隔離レベル「SERIALIZABLE」にする事だ。
隔離レベル「SERIALIZABLE」は最強の隔離レベルなのだ。
トランザクションの隔離レベルを「SERIALIZABLE」にしてみる No1 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> DELETE FROM tran1 ;
DELETE 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
後発のトランザクションT2がレコードを消去して「COMMIT」しても
御札(最強の隔離レベル)にしたため、先発のトランザクションT1は
T2が行った消去の影響を受けずにレコードが閲覧できる。
|
同様に、レコードがいきなり増える問題に対しても御札の力を見てみる。
トランザクションの隔離レベルを「SERIALIZABLE」にしてみる No2 |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> INSERT INTO tran1 VALUES ( 2 , 100 );
INSERT 733643 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
後発のトランザクションT2がレコードを追加して「COMMIT」しても
御札(最強の隔離レベル)にしたため、先発のトランザクションT1は
T2が行った追加の影響を受けずにレコードが閲覧できる。
|
見事のファントムリードを抑え込んでいる。
そう、御札の力は、つおいのらー!!
しかし、御札(最強の隔離レベル)は、お化けの動きを止めるものであって
除霊の力はない。なので、以下のように、お化けの力は健在だったりする。
隔離レベルが「SERIALIZABLE」でもお化けは健在なのだ |
|
トランザクション T1 |
トランザクション T2 |
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> SELECT * FROM tran1 ;
a | b
---+----
1 | 10
(1 row)
trandb=> UPDATE tran1 SET b = 20 ;
ERROR: Can't serialize access due to concurrent update
trandb=>
|
trandb=> BEGIN ;
BEGIN
trandb=> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
SET
trandb=> DELETE FROM tran1 ;
DELETE 1
trandb=> COMMIT ;
COMMIT
trandb=>
|
後発のトランザクションT2がレコードを消去し「COMMIT」をしたため
レコードが消えてしまっている。
隔離レベルが最強の場合、先発のトランザクションT1は
T2が消去したレコードの閲覧は可能なのだが、更新を行おうとしても
エラーが発生する。
|
上のような感じで、トランザクションの隔離レベルを最強にしても
問題が発生する。
なので、トランザクションを同時実行するためには、MVCC、ロック、
隔離レベルを上手に組み合わせ、データに矛盾が生じないように
データベースシステムを構築する必要がある。
知識の整理のため、もう一度、トランザクションの隔離レベルの表を出してみる。
トランザクションの隔離レベル |
隔離 度合 |
隔離レベル名 |
ダーティー リード |
反復不能 読み取り |
ファントム リード |
PostgreSQLの サポート |
弱 |
READ UNCOMMITED |
可能性有 |
可能性有 |
可能性有 |
未 |
↓ |
READ COMMITED |
可能性有 |
可能性有 |
安全 |
対応 |
↓ |
REPEATABLE READ |
可能性有 |
安全 |
安全 |
未 |
強 |
SERIALIZABLE |
安全 |
安全 |
安全 |
対応 |
一通り勉強してみると、この表の意味がわかる。
■■■ ここで暴露します ■■■
今回の勉強で、PostgreSQLに限らず、データベースを使った
システム構築を行う場合、複数のトランザクションを同時実行させ
しかもデータに矛盾が生じないようにする必要がある事を痛感した。
PostgreSQLを触って5年半経って、ようやくわかったのらー!!
我ながら学習速度が遅過ぎるのだが、事務員なので、それでも良いのだと
勝手に言い切ってみる (^^)
今まで、PHP言語を使って、PostgreSQLを使ったプログラムを書いてきた。
しかし、トランザクションの事なんぞ全く考えていなかった。
なので、明示的にトランザクションの開始の「BEGIN」や
「COMMIT」、「ROLLBACK」なんぞ使った事がない。
なので、こんな風な事を平気でやっていた事を書く事にします。
例えば、売上処理があったとします。
その際、売上明細データを追加を行うと共に、在庫データから
売上個数分を引く必要があります。
この場合、以下のテーブルの関係になります。
売上明細データと在庫データとの連動を考えた場合 |
|
売上処理を行う際、売上明細テーブルに明細レコードを追加するだけでなく
在庫データから売上した商品の数だけ、在庫の数を引く必要がある。
|
売上が計上されるごとに、2つのテーブルに接続して、
売上明細追加と、在庫の数を引く処理を行う必要があります。
今までは以下のような処理をしていました。
売上処理プログラム (あくまでも例です) |
|
こんなプログラムを平気で書いていました (^^;; |
<?php
$con = pg_connect("dbname=URIAGEDB");
if ( !$con )
{
echo "データベースにつながらへん";
exit ;
}
// 売上明細の追加のSQL文
$SQL1 = "INSERT INTO URIAGE VALUES ( $ID , $SHID , $KAZU , $DATE )";
// 在庫データの数を引くためのSQL文
$SQL2 = "UPDATE ZAIKO SET ZAKAZU = ZAKAZU - $KAZU";
// SQL文の処理
pg_exec($con , $SQL1 );
pg_exec($con , $SQL2 );
pg_close($con);
?>
|
明示的にトランザクションの開始「BEGIN」や正常終了「COMMIT」、
異常終了「ROLLBACK」は使っていない。
異常終了が起こった際の、エラー処理がないため、エラーが発生しても、
プログラムは動き続けるという代物を書いていたのだった。
|
正常にプログラムが動いていれば問題なさそうなのだが、
テーブルへの接続ができなかった場合、厄介な事が起こる。
こんな厄介な問題が発生する |
|
個々のテーブルに接続して更新をかけるプログラムなため、
もし、在庫データへの接続に失敗した時、売上明細データは追加されたが
在庫データで、売上個数分の在庫を引く作業の処理が行われないため、
データの整合性が取れなくなる。
在庫が合わなくなるため、データに信頼性がなくなってしまう問題が起こる。
|
なので、以下のようにエラー処理をキチンと行う必要がある。
エラー処理を追加した売上処理プログラム (あくまでも例です) |
|
こんなプログラムに書きかえます |
<?php
$con = pg_connect("dbname=URIAGEDB");
if ( !$con )
{
echo "データベースにつながらへん";
exit ;
}
// BEGINを明示的に宣言
pg_exec($con , "BEGIN" );
// 売上明細の追加のSQL文
$SQL1 = "INSERT INTO URIAGE VALUES ( $ID , $SHID , $KAZU , $DATE )";
// 在庫データの数を引くためのSQL文
$SQL2 = "UPDATE ZAIKO SET ZAKAZU = ZAKAZU - $KAZU";
// 売上明細データのSQL実行処理
$result1 = pg_exec($con , $SQL1 );
if ( $result1 ) // エラー処理
{
pg_exec($con , "ROLLBACK" );
die("売上明細データに接続できず、コケたのらー!!");
}
// 在庫データのSQL実行処理
$result2 = pg_exec($con , $SQL2 );
if ( $result2 ) // エラー処理
{
pg_exec($con , "ROLLBACK" );
die("在庫データに接続できず、コケたのらー!!");
}
// COMMITを明示的に宣言
pg_exec($con , "COMMIT" );
pg_close($con);
?>
|
明示的にトランザクションの開始や終了、異常時のエラー処理を追加したので
例え、片方のデータへの接続が失敗してもトランザクションの開始前に巻戻す
「ROLLBACK」が働く。そのため、データの不整合が起こるのを防ぐ。
|
さて、私が書いていたPHP言語のプログラムは、上のような基幹業務に関わる
重要な物ではなかったが、放置するのは良くないため、
明示的なトランザクションの開始と終了や、異常時のエラー処理の部分を
追加で書き加える事にした。
ただ、どのタイミングでロックをかけるのか、隔離レベルを上げるかの
サジ加減は全くわかっていないので、これからの学習になる。
中小企業で小規模なデータベースを使ったシステムの場合、
多数のトランザクションを同時並行で処理させる場合は少ない。
だが、「もしも」のための安全弁は必要だと感じた。
そういう意味では、データベースを使う大規模システムの手法は
「大は小を兼ねる」の言葉通り、中小企業にも使えると思うし、
できる限り使った方が良いと思った。
(2007/6/3追加)
ちょこちょことPostgreSQLが絡んだ社内システムを書き換えている間に
結構、ロック機能を使う例を見つけた。
さすがに勤務先で使っている例をそのまま紹介する事はできないので
一般論になるが、ロック機能を使う例を紹介します。
まずはテーブルロックを使う例を考えてみる事にした。
受注伝票、出荷伝票などを起票する場合、伝票番号をつける必要がある。
大抵の場合、伝票番号は連番になっている。
伝票番号を付ける場合を考える |
|
大抵の場合、連番になるので、上の図の状態だと、
次の伝票を発行する際は、「4」の伝票番号を付ける
そのため、伝票番号を割り当てる方法としては、
テーブル内の伝票番号の最大値を取得して、
その値に「1」を足した物にする。
|
今回、データベースの勉強をする前は、以下のような感じで
テーブル内から伝票番号の最大値を取得した後、
その値に「1」を足した番号を新規の伝票番号として発行し、
レコードを追加していた。
今までは、こんな風な事をしていた |
|
トランザクション機能は使っていなかった上、
ただ単に、SELECT文でテーブル内の伝票番号の最大値を取得し
その値に「1」を足した物を新規の伝票番号として割り当てていた。
|
だが、それでは問題が起こる事がわかった。
こんな問題が発生する |
|
上の図のように、ほぼ同時刻に2つの受注があった場合、
ロックをかけていないと、同じ受注番号を取得してしまう
問題が発生してしまう。
|
同じ伝票番号を取得してしまうのを防ぐ方法として
テーブルロック(排他ロック)が使える。
以下のように使えば良いのだ。
こんな対策を行う |
|
まずは、トランザクション機能を使う。
そして、伝票番号を取得する前にテーブルロック(排他ロック)をかける。
共有ロックの場合だと、複数のトランザクションの同時閲覧は可能なため
同じ伝票番号を取得していまう。そのため排他ロックを行う。
このテーブルロックは早い者勝ちなので、上図ではトランザクションT1が
先にロックをかける。そして、伝票番号を取得する。
トランザクションT2は、T1の終了待ちを行うため、
同じ伝票番号を取得する危険性を回避する事ができる。
|
これでテーブルロックをかける事例を紹介できた (^^)
次は、行ロックの例を考えるが、考え込んでしまった。
考え込んでも思いつかないので「逃げよう!」と思ったのだが、
ふと簡単な例が思いついたので紹介します。
それは在庫データの管理の話だ。
在庫データを考えてみる |
|
文房具店などで文具品を販売する際、在庫のあるなしを把握する必要がある。
「入荷」があれば在庫数は増え、出荷(販売)すれば在庫数は減る。
|
今まではこんな風な事をしていた |
|
トランザクション機能は使わず、単に出荷があれば、
上図のように、その度、SELECTで該当品目の在庫数を見て
在庫数に問題がなければ、UPDATEで更新作業を行っていた。
|
だが、上図のような場合だと、同時に複数の出荷があった場合に
以下の問題が発生する。
こんな問題が発生する |
|
ほぼ同時刻に出荷依頼がある場合を考えてみる。
トランザクションT1は3本出荷、T2は4本出荷依頼を行う。
ロックがかかっていない場合、ほぼ同時刻に2つのトランザクションが
在庫確認のために在庫数を読み込むと、同じ数字を読み込んでしまう。
共に「在庫は5本」と認識するために、在庫はあると判断して
共に在庫数を出荷数だけ減少させる。
その結果、在庫切れに気づかず、在庫数が「-2」で計上される。
|
在庫数が「-2」なんてありえへん!
在庫が切れているのにお客さんに販売ができるわけないのだ。
こんな事態を避けるために行ロックが使う方法がある。
こんな対策を行う |
|
在庫数を確認すると同時に行ロックをかけてしまう。
SELECT文の後ろに「FOR UPDATE」をつければ良いのだ。
ロックは早い者勝ちなので、早くロックをした人が処理を進められる。
最初に処理を進めたトランザクションT1は出荷が3本なので、
在庫数は大丈夫なので出荷が認められる。そして在庫数が3本引かれる。
T1がCOMMITで終了した後、ロックが解除されるので、T2がロックをかけて
処理を進める。
読み込んだ在庫数が「2」なので、4本出荷する場合、2本足らなくなる。
そこで「在庫が足らない」という事で、出荷を断念するように
知らせる事ができる。
|
ここで行ロックでなくても、テーブルロックでも使えるという意見も
あるかと思います。もちろん、テーブルロックでも問題ありません。
ただ、テーブルロックの場合、テーブル全体にロックがかかるため、
他の商品の在庫閲覧までできなくなります。
同時に、鉛筆とノートの出荷があった場合、どっちかの処理が終わらないと
前に進められなくなるため、同時実行を行えるようにするために、
行ロックの方が望ましいというわけだ。
ロックの使い方は、どんな場合に使うのかサジ加減は難しいと思っていたが
そんな事はなかった。
ただ、こんな簡単な事例ばかりではないため、大量の接続、大量の処理の場合、
処理速度を下げずに多くの処理を円滑に進められるかの点では
難しい判断を迫られると思ったりする。
(追加終わり)
まとめ
PostgreSQLの勉強をあまりしないまま、ズルズルと5年半を過ごしました。
あまり接続数が多くないシステムだったので、特に問題は起こらなかったのですが
今回のトランザクションの話、MVCC、ロック、隔離レベルを勉強して
今までズサンなシステム構築をしていたと思いました。
それと同時に、データベースを使った大規模システムの構築や運用している人達は
データの整合性を保つための知識や技術は凄いなぁと改めて感じました。
次章:「HTML、CSSって何?」を読む
前章:「ホームページの閲覧履歴の解析ソフト」(webalizer)を読む
目次:システム奮闘記に戻る