S-JIS[2021-09-18/2023-10-05]

Tsurugi select for updateの考察

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
y:200
3   update example
set x = 100 + 1, y = 200 + 1;
x=101
y=201
4   commit;  
5 T2 begin;  
6   select x, y from example; x:101
y:201
7   update example
set x = 101 + 10, y = 201 * 2;
x=111
y=402
8   commit;  
  T2→T1
処理 データ
1 T2 begin;  
2   select x, y from example; x:100
y:200
3   update example
set x = 100 + 10, y = 200 * 2;
x=110
y=400
4   commit;  
5 T1 begin;  
6   select x, y from example; x:110
y:400
7   update example
set x = 110 + 1, y = 400 + 1;
x=111
y=401
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
y:200
select x, y from example; x:100
y:200
3 update example
set x = 100 + 1, y = 200 + 1;
x=101
y=201
update example
set x = 100 + 10, y = 200 * 2;
x=110
y=400
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
for update;
x:100
y:200
    T1がロックしたので、
T2はロックが解除されるまで待つ。
3 update example
set x = 100 + 1, y = 200 + 1;
x=101
y=201
select x, y from example
for update;
 
4 commit;    
5     x:101
y:201
6     update example
set x = 101 + 10, y = 201 * 2;
x=111
y=402
 
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
y:200
select x, y from example; x:100
y:200
 
3 update example
set x = 100 + 1, y = 200 + 1;
x=101
y=201
update example
set x = 100 + 10, y = 200 * 2;
x=110
y=400
 
4 commit;        
5     commit;   OCCでは後からコミットした側がシリアライゼーションエラーになるので
トランザクションをリトライする。
6     begin;  
7     select x, y from example; x:101
y:201
 
8     update example
set x = 101 + 10, y = 201 * 2;
x=111
y=402
 
9     commit;    

Tsurugiでは、遅くともコミット時に、処理したデータが他のトランザクションによって更新されていないかどうかチェックされ、更新されている場合はシリアライゼーションエラー(リトライ可能な アボート)が発生する。
シリアライゼーションエラーが発生したら、そのトランザクションの処理を先頭から再実行(リトライ)すれば、今度は(同時に実行される競合トランザクションは無くなっているので)コミットが成功する。

LTXの場合は以下のような挙動になる。

  T1(LTX) T2(LTX) 備考
処理 データ 処理 データ
1 begin;       LTXでは、先に開始したトランザクションの方が優先度が高い。
2     begin;  
3 select x, y from example; x:100
y:200
select x, y from example; x:100
y:200
 
4 update example
set x = 100 + 1, y = 200 + 1;
x=101
y=201
update example
set x = 100 + 10, y = 200 * 2;
x=110
y=400
 
5     commit;   T2の方が先にコミットしたが、優先度が高いLTX(T1)が終わるまで待つ。
6 commit;       T2はシリアライゼーションエラーになるので
トランザクションをリトライする。
7     begin;  
8     select x, y from example; x:101
y:201
 
9     update example
set x = 101 + 10, y = 201 * 2;
x=111
y=402
 
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
for update;
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の順で実行したのと同じ結果になっている。


Tsurugiへ戻る / 技術メモへ戻る
メールの送信先:ひしだま