S-JIS[2021-09-18/2023-10-05]
Tsurugiでselect for updateに相当する処理について。
トランザクション分離レベルREAD COMMITTED(PostgreSQLやOracleのデフォルト)でよく使われるselect for updateで排他する方式(悲観ロック)は、Tsurugiでは使わない(使えない)。
Tsurugiのトランザクション分離レベルはSERIALIZABLEなので、トランザクションの処理内容が競合するとシリアライゼーションエラー(リトライ可能な
アボート)になる。その際に再実行(リトライ)するという方式(楽観ロック)がTsurugiの方式。
(リトライはアプリケーション側で行う必要がある。Javaの場合、Iceaxeにリトライする仕組みが用意されている)
理論的には、悲観ロックでも楽観ロックでも同じ結果になるらしい。
後述の例のように、いくつかを試してもその通りになる。
しかしプログラミング方法や挙動は異なる。
TODO+++---Tsurugiでは、for updateは実際どう扱われるか?(エラーになりそう)
2つのトランザクションT1とT2を同時に実行する例を考えてみる。
SERIALIZABLEの場合、T1→T2またはT2→T1の順で実行したのと同じ結果になるはずなので、悲観ロックと楽観ロックが同じ結果になることを確認したい。
例として、以下のようなテーブルとデータがあるものとし、T1は「xを+1」「yを+1」、T2は「xを+10」「yを2倍」する処理とする。
create table example ( k int primary key, x int, y int ); insert into example values(1, 100, 200);
このとき、T1とT2を直列に実行すると、T1→T2およびT2→T1はそれぞれ以下のようになる。
T1→T2 | |||
---|---|---|---|
処理 | データ | ||
1 | T1 |
begin; |
|
2 | select x, y from example; |
x:100 |
|
3 | update example |
x=101 |
|
4 | commit; |
||
5 | T2 |
begin; |
|
6 | select x, y from example; |
x:101 |
|
7 | update example |
x=111 |
|
8 | commit; |
T2→T1 | |||
---|---|---|---|
処理 | データ | ||
1 | T2 |
begin; |
|
2 | select x, y from example; |
x:100 |
|
3 | update example |
x=110 |
|
4 | commit; |
||
5 | T1 |
begin; |
|
6 | select x, y from example; |
x:110 |
|
7 | update example |
x=111 |
|
8 | commit; |
xは111となり、yは402または401となる。
(つまり、SERIALIZABLEで並列に実行すればこれと同じ結果になるはず、ということ)
次に、READ COMMITTED(排他なし)でT1とT2を並列に実行することを考えてみる。
T1 | T2 | |||
---|---|---|---|---|
処理 | データ | 処理 | データ | |
1 | begin; |
begin; |
||
2 | select x, y from example; |
x:100 |
select x, y from example; |
x:100 |
3 | update example |
x=101 |
update example |
x=110 |
4 | commit; |
|||
5 | commit; |
この場合、T1のコミットがT2より遅ければxが101・yが201、逆であればxが110・yが400で、T1・T2を順番に実行(T1→T2またはT2→T1)したのとは全く違う結果になってしまう。
ただ、こういう処理の場合、READ COMMITTEDではselect for updateでロックする。
そうすると以下のようになる。
T1 | T2 | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
begin; |
|||
2 | select x, y from example |
x:100 |
T1がロックしたので、 T2はロックが解除されるまで待つ。 |
||
3 | update example |
x=101 |
select x, y from example |
||
4 | commit; |
||||
5 | x:101 |
||||
6 | update example |
x=111 |
|||
7 | commit; |
これなら、T1・T2を順番に実行したのと同じ結果になる。
同じ処理を、Tsurugi(SERIALIZABLE)で実行するとどうなるか?
Tsurugiでは更新を伴うトランザクションの実行方法(トランザクション種別)はOCCとLTXの2種類があるが、OCCの挙動は以下のようになる。
T1(OCC) | T2(OCC) | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
begin; |
|||
2 | select x, y from example; |
x:100 |
select x, y from example; |
x:100 |
|
3 | update example |
x=101 |
update example |
x=110 |
|
4 | commit; |
||||
5 | commit; |
OCCでは後からコミットした側がシリアライゼーションエラーになるので トランザクションをリトライする。 |
|||
6 | begin; |
||||
7 | select x, y from example; |
x:101 |
|||
8 | update example |
x=111 |
|||
9 | commit; |
Tsurugiでは、遅くともコミット時に、処理したデータが他のトランザクションによって更新されていないかどうかチェックされ、更新されている場合はシリアライゼーションエラー(リトライ可能な
アボート)が発生する。
シリアライゼーションエラーが発生したら、そのトランザクションの処理を先頭から再実行(リトライ)すれば、今度は(同時に実行される競合トランザクションは無くなっているので)コミットが成功する。
LTXの場合は以下のような挙動になる。
T1(LTX) | T2(LTX) | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
LTXでは、先に開始したトランザクションの方が優先度が高い。 | |||
2 | begin; |
||||
3 | select x, y from example; |
x:100 |
select x, y from example; |
x:100 |
|
4 | update example |
x=101 |
update example |
x=110 |
|
5 | commit; |
T2の方が先にコミットしたが、優先度が高いLTX(T1)が終わるまで待つ。 | |||
6 | commit; |
T2はシリアライゼーションエラーになるので トランザクションをリトライする。 |
|||
7 | begin; |
||||
8 | select x, y from example; |
x:101 |
|||
9 | update example |
x=111 |
|||
10 | commit; |
LTXの場合、優先度が低い側が先にコミットすると、優先度が高いLTXが終わるまで待つ。
優先度が高いLTXが終わったら、処理したデータが他のトランザクションによって更新されていないかどうかチェックされ、更新されている場合はシリアライゼーションエラー
(リトライ可能なアボート)が発生する。
シリアライゼーションエラーが発生したら、そのトランザクションの処理を先頭から再実行(リトライ)すれば、今度は(同時に実行される競合トランザクションは無くなっているので)コミットが成功する。
OCCもLTXも、多少の違いはあれど、シリアライゼーションエラーが発生したらトランザクションを再実行(リトライ)することで、最終的にT1・T2を順番に実行したのと同じ結果になっている。
これがSERIALIZABLE(Tsurugi)の挙動である。
T1が同じデータを2回読み、T2でそのデータを更新する例を考えてみる。
以下のようなテーブルとデータがあるものとする。
create table example ( k int primary key, x int ); insert into example values(1, 100);
T1とT2を直列に実行すると、T1→T2およびT2→T1はそれぞれ以下のようになる。
T1→T2 | |||
---|---|---|---|
処理 | データ | ||
1 | T1 |
begin; |
|
2 | select x from example; |
x:100 |
|
3 | select x from example; |
x:100 |
|
4 | commit; |
||
5 | T2 |
begin; |
|
6 | update example
set x = x + 1; |
x=101 |
|
7 | commit; |
T2→T1 | |||
---|---|---|---|
処理 | データ | ||
1 | T2 |
begin; |
|
2 | update example
set x = x + 1; |
x=101 |
|
3 | commit; |
||
4 | T1 |
begin; |
|
5 | select x from example; |
x:101 |
|
6 | select x from example; |
x:101 |
|
7 | commit; |
ご覧の通り、T1のselectは2回とも同じ値を読み出している。
では、READ COMMITTED(排他なし)でT1とT2を並列に実行するとどうなるか考えてみよう。
T1 | T2 | |||
---|---|---|---|---|
処理 | データ | 処理 | データ | |
1 | begin; |
|||
2 | select x from example; |
x:100 |
||
3 | begin; |
|||
4 | update example
set x = x + 1; |
x=101 |
||
5 | commit; |
|||
6 | select x from example; |
x:101 |
||
7 | commit; |
ちょっと恣意的だが、T1の2回のselectの間にT2で当該データを更新・コミットすると、T1のselectは、1回目と2回目で異なる結果になってしまう。
もちろんこれも、select for updateでロックすれば同じデータが読める。
T1 | T2 | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
||||
2 | select x from example |
x:100 |
|||
3 | begin; |
||||
4 | update example
set x = x + 1; |
ロックされているので、解除されるまで待つ。 | |||
5 | select x from example; |
x:100 |
|||
6 | commit; |
||||
7 | x=101 |
||||
8 | commit; |
一方TsurugiのOCCでは、以下のようになる。
T1(OCC) | T2(OCC) | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
||||
2 | select x from example; |
x:100 |
|||
3 | begin; |
||||
4 | update example
set x = x + 1; |
x=101 |
|||
5 | commit; |
||||
6 | select x from example; |
x:101 |
OCCでは、SQL実行時点でコミットされている最新データを読む。 | ||
7 | commit; |
T1はシリアライゼーションエラーになるので トランザクションをリトライする。 |
|||
8 | begin; |
||||
9 | select x from example; |
x:101 |
|||
10 | select x from example; |
x:101 |
|||
11 | commit; |
Tsurugiでは、コミット時にトランザクションの内容がSERIALIZABLEかどうか(今回のケースでは、selectが同じ値を読んでいるかどうか)をチェックする。
今回のT1の初回はシリアライゼーションエラー(リトライ可能なアボート)となり、リトライ後はselectが2回とも同じ値を読んでいる。
これにより、T2→T1の順で実行したのと同じ結果になっている。
※このように、Tsurugiではselectのみのトランザクションであってもアボートすることがある(ので、必ずコミットして、コミットが成功することを確認しなければならない)。
LTXの場合は以下のようになる。
T1(LTX) | T2(LTX) | 備考 | |||
---|---|---|---|---|---|
処理 | データ | 処理 | データ | ||
1 | begin; |
LTXでは、先に開始したトランザクションの方が優先度が高い。 | |||
2 | select x from example; |
x:100 |
|||
3 | begin; |
||||
4 | update example
set x = x + 1; |
x=101 |
|||
5 | commit; |
優先度が低いT2は、優先度が高いT1がコミットされるまで待つ。 | |||
6 | select x from example; |
x:100 |
LTXでは、トランザクション開始時点の値を読む。 | ||
7 | commit; |
||||
8 | T1がコミットされ、(T2が更新したデータはT1により更新されなかったので競合なしと判断され、)T2のコミットも完了する。 |
この場合、T1→T2の順で実行したのと同じ結果になっている。