Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializable分離レベルで一意性違反を生じるアプリケーションの例 #3077

Open
tmitanitky opened this issue Sep 19, 2024 · 4 comments

Comments

@tmitanitky
Copy link

問題の詳細

https://www.postgresql.jp/document/16/html/transaction-iso.html#XACT-SERIALIZABLE

For example, imagine an application that asks the user for a new key and then checks that it doesn't exist already by trying to select it first, or generates a new key by selecting the maximum existing key and adding one.

は、

例えば、ユーザに新しいキーを聞いてからまずselectでそれがすでに存在しているか確かめるアプリケーション、もしくは存在している中で一番大きなキーを選択しそれに1を足すことで新しいキーを生成するアプリケーションを想像してみてください。

と訳されていますが、

「~なアプリケーションか、~なアプリケーション」ではなく、「~確かめ、もし存在していればすでに存在している最大のキーに1加えて新しいキーを生成するアプリケーション」という1つのアプリケーションを示しているのではないでしょうか。

「ユーザに新しいキーを聞いてからまずselectでそれがすでに存在しているか確かめるアプリケーション」では、一意性違反は生じないように思います。

提案:

例えば、ユーザに新しいキーを聞き、まずselectを試みることでそれがすでに存在していないことを確認するか、既存の最大キーをselectして1を足すことで新しいキーを生成するアプリケーションを想像してみてください。

確認した範囲では、9.6以降にある節のようです。

貢献者として記載可否

記載(貢献者欄に書いてください)

貢献者名

三谷知広

@tatsuo-ishii
Copy link
Contributor

結論から言うと、現在の訳で正しいと思います。
For example...の前の文を見ると、

This can be avoided by making sure that all Serializable transactions that insert potentially conflicting keys explicitly check if they can do so first.

とあります。For example..は、これを受けて2種類の例を提示し、前の文の"explicitly check if they can do so first. "を実施なければならない具体例2つを示していると考えられます。最初の例では、"trying to select it first"が"check"に相当します。後の例では、"selecting the maximum existing key and adding one"です。

具体的にSQLで例を示します。以下、T1, T2は2つのpsqlセッションを表していると考えてください。
create table t1(t text primary key, i int);
で作ったテーブルが存在するとします。
T1ではユーザから聞いたキー値が'p1'でした。T2では別のユーザからキー値を聞いたのですが、運悪くT1のユーザと同じ'p1'を選んでしまいました。この例では、T1もT2も最初にきちんと既存のキー値が存在するかどうか調べており(select * from t1 where t = 'p1';)、どちらも存在しないと言う答えが返ってきたので、T1では"insert into t1 values('p1', 1);"、T2では" insert into t1 values('p1', 2);"を実行しようとしています。結果、T1は成功しましたが、T2は直列化エラーとなりました。

T1: begin;
T1: set transaction isolation level serializable;
T1: select * from t1 where t = 'p1';
T2: begin;
T2: set transaction isolation level serializable;
T2: select * from t1 where t = 'p1';
T1: insert into t1 values('p1', 1);
T2: insert into t1 values('p1', 2);
T1: commit;	-- success
T2: cimmit;	-- abort
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during write.
HINT:  The transaction might succeed if retried.

では、T2がこのプロトコルを守らずに(つまり"select * from t1 where t = 'p1';"を実行しないで)、いきなりinsertしたらどうなるでしょうか?この場合、直列化エラーにならず、一意性違反になってしまいます。

もう一つの例"generates a new key by selecting the maximum existing key and adding one."では、たとえば以下のような例で確認できます。

T1: create table t2(i int primary key, j int);
T1: insert into t2 values(1, 1);
T1: begin;
T1: set transaction isolation level serializable;
T2: begin;
T2: set transaction isolation level serializable;
T1: insert into t2 select max(i) + 1, 2 from t2;
T2: insert into t2 select max(i) + 1, 2 from t2;
T1: commit;
T2: commit;
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during write.
HINT:  The transaction might succeed if retried.

この例で、T2がこのプロトコルを守らずに(つまり"insert into t2 select max(i) + 1, 2 from t2;"を実行しないで)、いきなり"insert into t2 values(2,2)"としたらどうなるでしょうか?この場合も、直列化エラーにならず、一意性違反になります。

@tatsuo-ishii
Copy link
Contributor

「ユーザに新しいキーを聞いてからまずselectでそれがすでに存在しているか確かめるアプリケーション」では、一意性違反は生じないように思います。

はい、その通りですが、原文の趣旨としては「selectでそれがすでに存在しているか確かめる」をしなければ一意性違反が生じる、ということだと思います。
"without following this protocol"の"this protocol"は、"checks that it doesn't exist already by trying to select it first"あるいは"selecting the maximum existing key and adding one."を指していると考えられるので。

@tmitanitky
Copy link
Author

詳細にありがとうございます。
「現在の訳で正しい」点、理解しました。
T2がいきなりinsertして、そのままcommitするケースを想定できていませんでした。

T1: begin;
T1: set transaction isolation level serializable;
T1: select * from t1 where t = 'p1';
T2: begin;
T2: set transaction isolation level serializable;
-- T2: select * from t1 where t = 'p1';
T1: insert into t1 values('p1', 1);
T2: insert into t1 values('p1', 2); -- T1 commitまで待機
T1: commit;	-- success
T2: -- ERROR:  duplicate key value violates unique constraint "t2_pkey"

このT2における一意性違反は、T1→T2とシリアル実行した結果と整合的です。

一方で、T2がselectなしに先にinsertし、そのまま先にcommitした場合、

T1: begin;
T1: set transaction isolation level serializable;
T1: select * from t1 where t = 'p1';
T2: begin;
T2: set transaction isolation level serializable;
-- T2: select * from t1 where t = 'p1';
T2: insert into t1 values('p1', 2);
T2: commit;	-- success
T1: insert into t1 values('p1', 1);
T1: -- ERROR:  duplicate key value violates unique constraint "t2_pkey"

T1: begin;
T1: set transaction isolation level serializable;
T1: select * from t1 where t = 'p1';
T2: begin;
T2: set transaction isolation level serializable;
-- T2: select * from t1 where t = 'p1';
T2: insert into t1 values('p1', 2);
T1: insert into t1 values('p1', 1); -- T2 commitまで待機
T2: commit;	-- success
T1: -- ERROR:  duplicate key value violates unique constraint "t2_pkey"

では、T1で一意性違反が発生します。これは、T1→T2(T2で一意性違反)や、T2→T1(そもそもselectで検出できるはず)のどちらの結果とも整合せず、これが「本当のシリアル実行では起こらないエラー」に相当するものと理解しました。

後者のアプリケーションでも、T1でのBEGIN後に、T2で直接insertしcommitしてから、T1でinsert into t2 select max(i) + 1, 2 from t2;を実行すると、一意性違反が発生しました。このT1での一意性違反は、T1→T2でも、T2→T1でも起こり得ないエラーでした。

お手を煩わせてしまい申し訳ありませんでした。
ドキュメントが充実していて大変助かっております。ありがとうございます。

@tatsuo-ishii
Copy link
Contributor

ご理解いただきありがとうございます。

お手を煩わせてしまい申し訳ありませんでした。

いえ、とんでもありません。私も勉強になりました。
ここはちょっと原文がわかりにくい気もします。何か例が付いていると良いのかもしれませんね。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants