#1. はじめに
OSS-DB Silver取得に向けて勉強中です。
トランザクションの4種の分離レベルについて、
それぞれの違いを言葉としては把握したものの 実際の挙動についてイメージしづらかったので
実際に試してみようと思いました。
◆やりたいこと◆
2つのセッションでトランザクションを開始して、
トランザクションの分離レベルの挙動の違いを確かめたい。
(Read uncommitted、Read committed、Repeatable read、Serializableの挙動の違いを確かめたい)
(ダーティーリード、ファジーリード、ファントムリード、直列化異常が起こる様子を再現したい)
◆つまずいたポイント◆
・1台のPCで2つのトランザクションを動かす方法がわからず、つまずいた。
→VSCodeでターミナルを2つ立ち上げて、それぞれでトランザクションを開始して解決。
・分離レベルの変更を、セッション1(S1)のみで行なったために、検証したい現象(ファジーリード)が起きないはずの場面で、ファジーリードが起きてしまった。
→セッション1(S1)とセッション2(S2)それぞれで分離レベルを変更して解決。
以下、作業記録です。
#2. 環境
バージョン | ||
---|---|---|
Host OS | Windows 10 Home | 21H1(OSビルド:19043.1165) |
Virtual Machine | WSL2 | - |
Remote OS | Ubuntu | 20.04.2 LTS (GNU/Linux 5.4.72-microsoft-standard-WSL2 x86_64) |
Database | PostgreSQL | 12.7 (Ubuntu 12.7-0ubuntu0.20.04.1) |
Database GUI | DBeaver | 21.3.0.202111281534 |
Docker | 無し | - |
#3. トランザクションの分離レベルとは
【参考】
・黒本『OSS-DB Silver[Ver.2.0]対応 問題集』p124.125 第5章トランザクション
・緑本『OSS教科書 OSS-DB Silver Ver.2.0対応 』p315-322 第10章トランザクション
・LPI-Japan「OSS-DB道場」
【トランザクション間で起こりうる現象】
現象 | 説明 |
---|---|
ダーティーリード | 他のトランザクションが更新してまだコミットしていないデータを読み込んでしまう。 自身のトランザクションから、別のトランザクションの未コミットな状態の挿入/更新/削除結果が見えてしまうこと。 |
反復不能読み取り (ノンリピータブルリード、 ファジーリード) |
トランザクション内で以前読み込んだデータを再度読み込んだときに、他のトランザクションでコミットされた更新内容が影響し、以前と異なる結果を得てしまう。 自身のトランザクションから、別のトランザクションのコミットされた更新/削除結果が見えてしまうこと。 |
ファントムリード | トランザクション内で以前読み込んだデータを再度読み込んだときに、他のトランザクションでコミットされた更新内容が影響し、以前存在しなかったデータを結果として得てしまう。 自身のトランザクションから、別のトランザクションのコミットされた挿入結果が見えてしまうこと。 |
直列化異常 | 複数のトランザクションのコミットの結果が、トランザクションを1つずつ重ならないように実行した場合と比較して、どのような順序を仮定しても違う結果になり、一貫性のない状態となってしまうこと。 |
【トランザクション分離レベル】
分離レベル | ダーティリード | 反復不能読み取り | ファントムリード | 直列化異常 |
---|---|---|---|---|
Read uncommitted | あり | あり | あり | あり |
Read committed (PostgreSQLのデフォルト) |
なし | あり | あり | あり |
Repeatable read | なし | なし | あり | あり |
Serializable | なし | なし | なし | なし |
↑下に行くほど分離性が強くなる |
・分離レベル=ACID特性の内の「分離性 (Isolation)」に関係する。
トランザクション同士の干渉を防ぐために必要な、個々のトランザクションの分離の強さのレベル。
・PostgreSQLがサポートする分離性=Read committed、Repeatable Read、Serializable
( 'READ UNCOMMITTED' に設定しても内部的には 'READ COMMITTED' と同じ動作となり、
ダーティーリードという望ましくない結果が起きないようになっている ←参考:「OSS-DB道場」)
・分離性が弱いほど性能上は有利になる。
(疑問:有利って、処理の速さについて?どの点において有利なのだろう?)
が、あるトランザクションが別のトランザクションの影響を受けてしまうため、整合性を保証することが難しくなる。
・分離レベルの設定は、SET TRANSACTIONコマンド、あるいはpostgresql.confのdefault_transaction_isolationなどで行うことが可能。
#4. 準備:1台のPC上で2つのトランザクションを開始し1つのテーブルに対してSQLを実行する方法
さあ、試してみよう!と思った矢先。
トランザクションの挙動を試すには、2つのPC?からテーブルにアクセスしてSQLを実行する必要があるんじゃないの!?
という疑問が浮かびました。
疑問:1台のPCで2つのトランザクションを動かすには、どうしたら良いの?
↓
結論:VSCodeでターミナルを2つ立ち上げて、それぞれでトランザクションを開始すればOK!
↓こちらの記事にまとめました。
①2つのターミナル名を「bash」→「S1/S2」にリネイム
ひとつめのターミナルをS1
ふたつめのターミナルをS2と名付けました。
③S2ターミナル(以下S2)でPostgreSQLに接続
(同上)
これで準備OKです。
#5. 実際に試してみる
コードは緑本『OSS教科書 OSS-DB Silver Ver2.0対応』第10章(トランザクション)の解説を参考にしました。
デフォルトの「Read Committed」になっています。
##1)Read uncommitted
◆Read uncommittedの特徴◆
ダーティーリード、ファジーリード、ファントムリード、直列化異常が起こる
ですが、実際には分離レベルを「Read uncommitted」に設定しても、ダーティーリードは起きません。
緑本ではこのことについて明記されていませんでしたが、LPI-JapanのOSS-DB道場では次のように書かれていました。
'READ UNCOMMITTED' に設定しても内部的には 'READ COMMITTED' と同じ動作となり、
ダーティーリードという望ましくない結果が起きないようになっている
(引用元:OSS-DB道場)
つまり、PostgreSQLではダーティーリードは起こり得ない、ということですね。
##2)Read committed(デフォルト)
◆Read committedの特徴◆
ファジーリード、ファントムリード、直列化異常が起こる
確かめてみます。
###2-a)ダーティーリードが起こらないことを確認(含 未解決疑問3)
⓪S1でテーブルを用意
S1で↓赤枠のテーブルを作成。
DROP TABLE IF EXISTS tbl;
を実行してから下記に進む!
<tblテーブル>
id | count |
---|---|
1 | 5 |
2 | 8 |
更新前の「id1--5」が見えました。
ダーティーリードが起きないことが確認できました。OKです。
⑤ABORT
ここではトランザクションはCOMMITさせず、ABORTしてトランザクション開始前の状態に戻しておきます。
<S1>
ROLLBACKと返ってきました。緑本(p.310)に「ROLLBACKとABORTは同義です」とありました。
ABORTしてもROLLBACKと返ってくるのですね。
(ABORTと返ってくるケースもあるのだろうか?→未解決の疑問3)として追記)
###2-b)ファジーリードが起こることを確認
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
④S2でtblテーブルを検索
SELECT * FROM tbl;
ダーティーリードは起きていません。
⑥S2でtblテーブルを検索
SELECT * FROM tbl;
S1のコミット済みの更新結果が見えてしまいました。
つまり、ファジーリードが起きました。
S1はコミット済みなので、UPDATE tbl SET count=5 WHERE id=1;
でトランザクション開始前の状態に戻しておきます。
S2はコミット前なので、ABORT;
でトランザクション開始前の状態に戻しておきます。
###2-c)ファントムリードが起こることを確認
⓪用意したテーブルはこちら
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
S1の③の更新結果は見えていない、つまり「ダーティーリードが起きていない」状態です。OKです。
⑤S1でtblテーブルにレコードを挿入
INSERT INTO tbl VALUES (1,12);
⑦S2のトランザクション内でtblテーブルをSELECT
SELECT * FROM tbl;
S2でトランザクション開始後、1回目にSELECTしたtblテーブルと2回目にSELECTしたtblテーブルの内容が異なる(=別のトランザクションで"新レコードを挿入&コミット"されたテーブルが見えてしまった)現象が起きました。
つまり、ファントムリードが起きました!
見えてはいけないものが見えてしまった、まさにファントム(幽霊)ですね。
###2-d)直列化異常が起こることを確認(含 未解決疑問4)
⓪用意したテーブルはこちら(赤枠)(S1で操作)
③S1にINSERT
INSERT INTO tbl SELECT 1,sum(count) FROM tbl WHERE id=2;
SELECT * FROM tbl;
④S2にINSERT
INSERT INTO tbl SELECT 2,sum(count) FROM tbl WHERE id=1;
SELECT * FROM tbl;
⑤S1のトランザクションをCOMMIT
COMMIT;
SELECT * FROM tbl;
⑥S2のトランザクションをCOMMIT
COMMIT;
SELECT * FROM tbl;
…直列化異常、起きませんでした。
緑本p317では、S2では「1|30」(S1のINSERTの結果)を取り込めていない状態になっていました。(つまり5rows)
緑本には、「S2をS1よりも先にコミットしても同様の結果となる。したがって直列化異常となる。」と書かれていました。
が、今回試した結果、直列化異常は起きませんでした。なぜ??
教科書の情報は古いのでしょうか?あるいは、何か操作を間違えたのでしょうか…。
はたまた、S2をCOMMITした際に、S1がCOMMITしている内容が見えているという今の状態(S1のCOMMIT内容もS2のCOMMIT内容も両方結果に反映されているという状態)が、つまり「直列化異常」なのでしょうか?S2をCOMMITした時点でエラーが出てS2のトランザクション内で実行した内容が取り消されれる、というのが、本来の「トランザクションが正常に機能している」状態だと思うので、そうなっていないということは、つまり「直列化異常」なのでしょうか?
(→未解決の疑問4 に追記)
##3)Repeatable read
◆Repeatable readの特徴◆
ファントムリード、直列化異常が起こる
確かめてみます。
###3-0)分離レベルの変更(Read Committed → Repeatable read)(含 未解決疑問2)
S1で、tblテーブルの分離レベルを
Read committed(PostgreSQLのデフォルト)
から Repeatable read
に変更する。
緑本(p318)の説明では以下の書式が紹介されていた。
◆セッション単位の分離レベルの指定方法◆
SET default_transaction_isolation TO '分離レベル';
◆トランザクション単位の分離レベルの指定方法◆
SET transaction_isolation TO '分離レベル';
BEGIN ISOLATION LEVEL 分離レベル;
またはSTART TRANSACTION ISOLATION LEVEL 分離レベル
「どちらを使うのが適切なのか」という判断はどこをポイントに考えればよいのか、
今はよくわかりません。(未解決の疑問 2. として追記)
トランザクション単位の指定をした場合、そのトランザクション中だけ指定した分離レベルとなり、COMMITやABORTにより元の分離レベルに戻る、とのことでした。
今回は、自分が変更するまでは指定した分離レベルを維持したいので、
セッション単位で指定します。
①S1で分離レベルをデフォルトの「Read Committed」から「Repeatable read」に変更
①S1で分離レベルを確認
SHOW transaction_isolation;
「Repeatable read」になっています。OKです。
###3-a)ダーティーリードが起こらないことを確認
更新前の「id1--5」が見えました。
ダーティーリードが起きないことが確認できました。OKです。
⑤ABORT
ここではトランザクションをCOMMITせず、ABORTしてトランザクション開始前の状態に戻しておきます。
<S1>
###3-b)ファジーリードが起こることを確認 →失敗(含 未解決疑問5)
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
④S2でtblテーブルを検索
SELECT * FROM tbl;
ダーティーリードは起きていません。
⑥S2でtblテーブルを検索
SELECT * FROM tbl;
S1のコミット済みの更新結果が見えてしまいました。
つまり、ファジーリードが起きました。
今の設定は「Repeatable read」なので、がファジーリードは起こらないはずです。
なぜ起きてしまったのでしょうか?
仮説:S2の分離レベルを変更していないことが原因なのではないか?
検証:S2の分離レベルを変更する
S1でSET default_transaction_isolation TO 'Repeatable read';
を実行しましたが
S2では実行しませんでした。(テーブルもS1でCREATE TABLEすればS2からも SELECT TABLEできるので、分離レベルもS1で設定すればS2にも反映されると思ったので)
S2の分離レベルを確認してみます。
⑦S2で分離レベルを確認
SHOW transaction_isolation;
デフォルトの「read committed」になっていました。
これが原因でした。
(実際、緑本p318でも「セッション単位の分離レベルの指定方法」の書式として
SET default_transaction_isolation TO '分離レベル';
が紹介されていました。きちんと「セッション単位の」と書かれていました。
その意味をきちんと理解できていませんでした。)
⑧S2で分離レベルをデフォルトの「Read Committed」から「Repeatable read」に変更
SET default_transaction_isolation TO 'Repeatable read';
⑨S2で分離レベルを確認
SHOW transaction_isolation;
「read committed」のままです。直前で「Repeatable read」への変更が「SET」されているのに、なぜでしょう?
仮説:トランザクションを終了していない状態では、分離レベルを変更できないのではないか?
検証:トランザクション終了させてから、もう一度分離レべルを確認してみる。
⑩S2のトランザクションをABORT→分離レベルを変更
S2はコミット前なので、ABORT;
でトランザクション開始前の状態に戻しておきます。
ABORT直後の分離レベルは「read committed」でしたが、
SET default_transaction_isolation TO 'Repeatable read';
で
「Repeatable read」に変更することができました!
◆わかったこと◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
・セッションを複数立ち上げた際の分離レベルの指定→個別のセッション毎に指定する必要がある
・トランザクション内ではSET default_transaction_isolation TO '分離レベル';
での分離レベルの指定はできない。
(疑問:トランザクション単位の指定方法であるSET transaction_isolation TO '分離レベル';
、START TRANSACTION ISOLATION LEVEL 分離レベル;
だと指定できるのだろうか?トランザクション開始直後であれば指定できそう。今回のように既にBEGIN後にSELECTした後にも指定できるのかどうかも、試してみたい。が、今は深追いしない。→未解決の疑問5 として追記)
◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
改めて、新たにファジーリードの挙動を試します。
###3-b)ファジーリードが起こることを確認 →成功
⓪S1でテーブルを用意 & S1,S2の分離レベルがRepeatable readであることを確認
<S1>
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
④S2でtblテーブルを検索
SELECT * FROM tbl;
ダーティーリードは起きていません。
⑥S2でtblテーブルを検索
SELECT * FROM tbl;
ファジーリードは起きていません。
今回は成功しました!
###3-c)ファントムリードが起こることを確認→失敗(含 未解決の疑問6)
⓪テーブルはこちら
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
S1の③の更新結果は見えていない、つまり「ダーティーリードが起きていない」状態です。OKです。
⑤S1でtblテーブルにレコードを挿入
INSERT INTO tbl VALUES (1,12);
⑦S2のトランザクション内でtblテーブルをSELECT
SELECT * FROM tbl;
ファントムリードは起きませんでした。
S1もS2も「Repeatable read」に設定されているので
ここでファントムリードが起きてS1のコミット済みのテーブル(3rows)が見えるはずだったのですが…。
なぜでしょう?(→未解決の疑問6に追記)
###3-d)直列化異常が起こることを確認→失敗(含 未解決の疑問4)
③S1にINSERT
INSERT INTO tbl SELECT 1,sum(count) FROM tbl WHERE id=2;
SELECT * FROM tbl;
④S2にINSERT
INSERT INTO tbl SELECT 2,sum(count) FROM tbl WHERE id=1;
SELECT * FROM tbl;
(ダーティーリードは起こっていない。)
⑤S1のトランザクションをCOMMIT
COMMIT;
SELECT * FROM tbl;
⑥S2のトランザクションをCOMMIT
COMMIT;
SELECT * FROM tbl;
またもや、直列化異常、起きませんでした。
なぜ??
はたまた、今の状態が「直列化異常が起きている状態」なのでしょうか?
(→未解決の疑問4 に追記)
##4)Serializable
◆Serializableの特徴◆
ダーティーリード、ファジーリード、ファントムリード、直列化異常のいずれも起こらない
試してみます。
###4-0)分離レベル変更&準備
①S1でDROP TABLE
DROP TABLE IF EXISTS tbl;
(S2ではDROP TABLEしなくてOK。もしS2でDROP TABLE IF EXISTS tbl;
を実行したら
「NOTICE: table "tbl" does not exist, skipping
DROP TABLE」というエラーが返ってくる。)
①S1とS2で分離レベルを「Serializable」に変更
SET default_transaction_isolation TO 'Serializable';
<S1><S2>それぞれで実施
###4-a)ダーティーリードが起こらないことを確認
更新前の「id1--5」が見えました。
ダーティーリードが起きないことが確認できました。OKです。
⑤ABORT
ここではトランザクションをCOMMITせず、ABORTしてトランザクション開始前の状態に戻しておきます。
<S1>
###2-b)ファジーリードが起こらないことを確認
⓪S1でテーブルを用意
<tblテーブル>
id | count |
---|---|
1 | 5 |
2 | 8 |
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
④S2でtblテーブルを検索
SELECT * FROM tbl;
ダーティーリードは起きていません。
⑥S2でtblテーブルを検索
SELECT * FROM tbl;
ファジーリードは起きていません。
OKです。
###4-c)ファントムリードが起こらないことを確認
⓪テーブルはこちら
③S1で id1のcount列 の値を 5→10 に更新
UPDATE tbl SET count=10 WHERE id=1;
S1の③の更新結果は見えていない、つまり「ダーティーリードが起きていない」状態です。OKです。
⑤S1でtblテーブルにレコードを挿入
INSERT INTO tbl VALUES (1,12);
⑦S2のトランザクション内でtblテーブルをSELECT
SELECT * FROM tbl;
ファントムリードは起きませんでした。
OKです。
###4-d)直列化異常が起こらないことを確認
③S1にINSERT
INSERT INTO tbl SELECT 1,sum(count) FROM tbl WHERE id=2;
SELECT * FROM tbl;
④S2にINSERT
INSERT INTO tbl SELECT 2,sum(count) FROM tbl WHERE id=1;
SELECT * FROM tbl;
(ダーティーリードは起こっていない。)
⑤S1のトランザクションをCOMMIT
COMMIT;
SELECT * FROM tbl;
⑥S2のトランザクションをCOMMIT
COMMIT;
すると、エラーメッセージが出ました。
【エラー文】
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
<Google翻訳>
トランザクション間の読み取り/書き込みの依存関係のため、アクセスをシリアル化できませんでした
詳細:理由コード:コミットの試行中に、ピボットとしての識別でキャンセルされました。
ヒント:再試行すると、トランザクションが成功する可能性があります。
SELECT TABLEして状況を確かめると、S1でコミットしたテーブルが表示されました。
(S2でINSERTした id=2,count=30 のレコードはエラーが出てCOMMITされず反映されなかった、という状況です。)
一貫性のある状態になっています。
つまり、直列化異常が起きなかった。トランザクションがしっかりと機能した。
ということですね。OKです。
#6.疑問に思ったが解決した事
##1.分離レベルの対象はDB全体?テーブル単体?
分離レベルのことを考えている最中、
分離レベルの対象は、テーブルなのだろうか?ossdbデータベースなのだろうか?どちらなんだろう?
という疑問が浮かびました。
分離レベルの設定をする際に、この疑問の答えを見つけました。
セッション単位の分離レベルの設定は次のとおりです。(中略)
トランザクション単位の分離レベルの指定方法は次のとおりです。(後略)
(緑本『OSS教科書 OSS-DB Silver Ver2.0対応』p318 10章トランザクション)
DB単位・テーブル単位ではなく、「セッション単位・トランザクション単位」であり、
それぞれ設定方法が用意されている。
セッションとは、接続のこと。
#7.未解決の疑問
##1.「分離性が弱いほど性能上は有利になる」というのは「処理の速度」について?
「分離性が弱いほど性能上は有利になる」とのことだが、処理が速くなる、ということ?「性能上」とは、どの性能について言っているんだろう?「速度」のことを言っている、という理解で合っているのだろうか?
##2.分離レベルの設定の単位
セッション単位で指定する方法と、トランザクション単位でする方法が存在している。
どういった場合にそれぞれを指定するのが良いのか、
指定する単位を選ぶ際の基準がわからない。
##3.ABORTって返ってくるの?
トランザクション途中で、トランザクション開始前の状態に戻したいと思いABORT;
したところ、「ROLLBACK」と返ってきました。緑本(p.310)に「ROLLBACKとABORTは同義です」とありました。
ABORTしてもROLLBACKと返ってくるのですね。
ABORTと返ってくるケースもあるのでしょうか?
##4.Read committed・Repeatable readでは直列化異常は起きない?
緑本p317にはRead uncommitted、Read committed、Repeatable readの3つの分離レベルで「直列化異常」が起きる、と書かれていた。が、実際に「Read committed」「Repeatable read」で挙動を試した結果、直列化異常は起きなかった。なぜ?実際の挙動とPosgreSQLの分離レベルの定義にはズレがあるのでしょうか?
はたまた、S2をCOMMITした際に、S1がCOMMITしている内容が見えているという状態(S1のCOMMIT内容もS2のCOMMIT内容も両方結果に反映されているという状態)が、つまり「直列化異常」なのでしょうか?S2をCOMMITした時点でエラーが出てS2のトランザクション内で実行した内容が取り消されれる、というのが、本来の「トランザクションが正常に機能している」状態だと思うので、そうなっていないということは、つまり「直列化異常」なのでしょうか?
##5.トランザクション中の分離レベルの変更は可能?
トランザクション単位の指定方法であるSET transaction_isolation TO '分離レベル';
、START TRANSACTION ISOLATION LEVEL 分離レベル;
だと指定できるのだろうか?トランザクション開始直後であれば指定できそう。今回のように既にBEGIN後にSELECTした後にも指定できるのかどうかも、試してみたい。が、今は深追いしない。
##6.Repeatable readでファントムリードが起きないことってあるの?
S1もS2も「Repeatable read」に設定されていて
ファントムリードが起きるはずの場面で、ファントムリードが起きなかった。
なぜ?(→2-c)
#8.参考