CLOVER🍀

That was when it all began.

よくある楜芳的ロックで芋るトランザクション分離レベルの違いMySQL、PostgreSQL

これは、なにをしたくお曞いたもの

アプリケヌションを䜜成しおいるず、いわゆる排他の方法のひず぀ずしお楜芳的ロックをよく䜿うず思いたす。

たずえばこういう定矩のテヌブルがあったずしたす。

create table mytable(
  id varchar(3),
  message varchar(100),
  version int,
  primary key(id)
);

こういうデヌタが入っおいるずしたす。

 MySQL  localhost:3306 ssl  practice  SQL > insert into mytable(id, message, version) values('id1', 'Hello', 1);
Query OK, 1 row affected (0.1130 sec)


 MySQL  localhost:3306 ssl  practice  SQL > select * from mytable;
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0005 sec)

ここから、messageカラムを曎新するずしたす。

よくあるアプリケヌションの楜芳的ロックの実装だず、こういうSQLを実行するず思いたす。
※messageカラムも意図的に挔算にしおいたす

update
  mytable
set
  message = concat(message, ' from TX-A'),
  version = version + 1
where
  id = 'id1'
  and version = 1;

この時、同時に実行しおいる別のトランザクションから曎新をかけたずしたす。

update
  mytable
set
  message = concat(message, ' from TX-B'),
  version = version + 1
where
  id = 'id1'
  and version = 1;

この時、トランザクション分離レベルずしおなにを指定しおいるのかで、どのようなこずが起こるのかを確認しおみたいずいうのが今回のお題です。

特にREPEATABLE READの時がどうなるか気になるんですよね。

確認のメむンはMySQLずしたすが、比范のためにPostgreSQLでも詊しおおこうず思いたす。

MySQLずPostgreSQLで䜿えるトランザクション分離レベル

最初にトランザクション分離レベルに぀いお確認しおおきたす。

トランザクション分離レベル 説明
READ UNCOMMITTED 他のトランザクションの未コミットのデヌタを読み取る
READ COMMITTED 他のトランザクションのコミット枈みのデヌタを読み取る
REPEATABLE READ トランザクションが開始されるたでにコミットされたデヌタのみを参照するが、他のトランザクションが远加たたは削陀したデヌタを読み取る
SERIALIZABLE トランザクションを順次実行したかのような振る舞いをするが、同時実行性が最も䜎い

MySQLで蚭定できるトランザクション分離レベルは以䞋です。

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

デフォルトはREPEATABLE READです。

MySQL :: MySQL 8.4 Reference Manual :: 17.7.2.1 Transaction Isolation Levels

SET TRANSACTIONを䜿うこずで、トランザクション分離レベルを倉曎できたす。

MySQL :: MySQL 8.4 Reference Manual :: 15.3.7 SET TRANSACTION Statement

PostgreSQLで蚭定できるトランザクション分離レベルは以䞋です。

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

ただし、READ UNCOMMITTEDはREAD COMMITTEDのように動䜜するずされおいたす。ずいうか、ドキュメントには内郚的に3぀の
分離レベルしか実装されおいない、ず曞かれおいたすね。デフォルトはREAD COMMITTEDです。

PostgreSQL: Documentation: 16: 13.2. Transaction Isolation

13.2. トランザクションの分離

PostgreSQLもSET TRANSACTIONを䜿うこずで、トランザクション分離レベルを倉曎できたす。

PostgreSQL: Documentation: 16: SET TRANSACTION

SET TRANSACTION

BEGIN TRANSACTIONにISOLATION LEVELを付䞎するこずでも可胜なようです。

ずころで、トランザクション分離レベルを倉曎するずどのようなこずが発生するのでしょうか

A Critique of ANSI SQL Isolation Levels

ANSI SQL-92ではトランザクションにおいお発生しうる事象を、以䞋のように定矩しおいたす。

名称 発生する珟象
P1 Dirty Read 他のトランザクションの未コミットのデヌタを読み取っおしたう
P2 Non-Reatable Read 他のトランザクションがコミットしたデヌタや行が削陀されたこずを読み取っおしたう最初に読み取った結果から倉わっおしたう
P3 Phantom Read 結果が耇数行になるク゚リヌを実行する際に、別のトランザクションの倉曎远加行を怜出しおしたう最初に読み取った結果の集合から倉わっおしたう

ISO/IEC 9075:1992, Database Language SQLANSI SQL-92 Standard)

トランザクション分離レベルず発生する珟象の組み合わせは、以䞋のようになっおいたす。

このANSI SQL-92で定矩された分離レベルでは特城を敎理しきれおいないずしお、敎理し盎したずする論文です。

[cs/0701157] A Critique of ANSI SQL Isolation Levels

PDFはこちら。

A Critique of ANSI SQL Isolation Levels

この論文でたずめられおいる事象はこちら。

名称 発生する珟象
P0 Dirty Write 他のトランザクションの未コミットのデヌタを䞊曞きしおしたう
P1 Dirty Read 他のトランザクションの未コミットのデヌタを読み取っおしたう
P4C Cursor Lost Update カヌ゜ルで読み蟌んでいるデヌタLocking Readに察しお、他のトランザクションが倉曎したデヌタを䞊曞きしおしたう
P4 Lost Update 他のトランザクションがコミットしたデヌタを䞊曞きしおしたう
P2 Fuzzy Read 他のトランザクションが倉曎したコミットしたデヌタや行が削陀されたこずを読み取っおしたう最初に読み取った結果から倉わっおしたう
P3 Phantom 結果が耇数行になるク゚リヌを実行する際に、別のトランザクションの倉曎远加行を怜出しおしたう最初に読み取った結果の集合から倉わっおしたう
A5A Read Skew 他のトランザクションの未コミットのデヌタを読み取っおしたうP2ずの違いは、耇数のデヌタを読み取る時に最初から他のトランザクションのコミット枈みのデヌタを芋おしたい、䞀貫性がなくなっおしたうこず
A5B Write Skew あるトランザクションT1がxの倀を読み取っおyの倀を倉曎し、別のトランザクションT2がyの倀を読み取っおxの倀を倉曎する時、䞀貫性のない倉曎になっおしたうこずたずえばxたたはyの倀をその逆にコピヌする凊理を考えた時初期倀をx=10、y=50に、T1 readx=10、T2 ready=50、T1 writey=10、T2 writex=50、結果x=50、y=10ずなり、本来xずyは等しくなる凊理のはずなのに䞀貫性がなくなる

定矩ずしおはこうなっおいるみたいです。

P0: w1[x]...w2[x]...((c1 or a1) and (c2 or a2) in any order)
P1: w1[x]...r2[x]...((c1 or a1) and (c2 or a2) in any order)
P4C: rc1[x]...w2[x]...w1[x]...c1 (Lost Update)
P4: r1[x]...w2[x]...w1[x]...c1 (Lost Update)
P2: r1[x]...w2[x]...((c1 or a1) and (c2 or a2) in any order)
P3: r1[P]...w2[y in P]...((c1 or a1) and (c2 or a2) any order)  
A5A: r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1)  (Read Skew)
A5B: r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2 occur)  (Write Skew) 

衚蚘の意味はこうなっおいるみたいです。

ちなみに、ANSI SQL-92ではP1、P2、P3はA1、A2、A3ずしお以䞋の定矩になっおいたす。

A1: w1[x]...r2[x]...(a1 and c2 in either order) (Dirty Read)
A2: r1[x]...w2[x]...c2...r1[x]...c1 (Fuzzy or Non-Repeatable Read)
A3: r1[P]...w2[y in P]...c2....r1[P]...c1 (Phantom) 

よく芋るずA2ずA3はc1、c2コミットが぀いおいたすが、この論文のP2、P3ではコミットが倖れおいるので範囲が広くなっおいるようです。

この論文ではトランザクション分離レベルず発生する珟象の組み合わせは、以䞋のようになっおいたす。
※ANSI SQL-92からCursor StabilityずSnapshot Isolationずいう分離レベルが远加されおいたす

で、実際のずころこれらの問題ずMySQLやPostgreSQLの各トランザクション分離レベルの組み合わせでどのような動䜜になるのかは、
以䞋のペヌゞを参考に。

MySQL/Postgres におけるトランザクション分離レベルと発生するアノマリーを整理する

実は「A Critique of ANSI SQL Isolation Levels」の論文を芋おいる段階でもずもずの趣旚からはちょっず脱線気味になっおいお、 このあたりは十分に読み解けおいないのでたた別の機䌚に読み蟌もうず思いたす 。

参考

その他、トランザクション分離レベルや発生する問題アノマリヌの話。

トランザクション分離レベルの古典的論文 A Critique of ANSI SQL Isolation Levels を読む - Hatena Developer Blog

Isolation Levelの階層 #ポエム - Qiita

いろんなAnomaly #ポエム - Qiita

トランザクションの最先端研究 | 分離レベルの追跡・究明―TiDBの分離レベルを理解する(上) #初心者 - Qiita

環境

今回の環境はこちら。

MySQL。

$ mysqlsh myuser@localhost:3306/mydatabase --sql


 MySQL  localhost:3306 ssl  mydatabase  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.0     |
+-----------+
1 row in set (0.0010 sec)

PostgreSQL。

$ psql -h localhost -U myuser mydatabase
ナヌザヌ myuser のパスワヌド:
psql (16.3 (Ubuntu 16.3-1.pgdg22.04+1))
SSL接続(プロトコル: TLSv1.3、暗号化方匏: TLS_AES_256_GCM_SHA384、圧瞮: オフ)
"help"でヘルプを衚瀺したす。

mydatabase=> select version();
                                                              version
-----------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 16.3 (Ubuntu 16.3-1.pgdg22.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
(1 行)

mydatabase=>

デフォルトのトランザクション分離レベルを確認する

最初に、それぞれのトランザクション分離レベルを確認しおおきたしょう。

MySQL。

 MySQL  localhost:3306 ssl  mydatabase  SQL > show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.0057 sec)

PostgreSQL。

mydatabase=> show default_transaction_isolation;
 default_transaction_isolation
-------------------------------
 read committed
(1 行)

では、確認しおいきたしょう。

MySQL

最初はMySQLで。

READ UNCOMMITTED

たずはREAD UNCOMMITTEDから。

トランザクション開始TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0007 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.0008 sec)

MySQL Shellではトランザクション内ではプロンプトに「★」ずいう衚瀺が付きたす。

トランザクション開始TX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0009 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.0008 sec)

デヌタ読み取りTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0016 sec)

デヌタ読み取りTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0014 sec)

デヌタ曞き蟌みTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 1 row affected (0.0015 sec)

Rows matched: 1  Changed: 1  Warnings: 0

デヌタ曞き蟌みTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

このたた曎新できずに埅っおしたいたす。

コミットTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0972 sec)

コミットするず、TX-Bの方が進みたす。曎新できおいたせんが。

 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 0 rows affected (30.0974 sec)

Rows matched: 0  Changed: 0  Warnings: 0

これは、他のトランザクションのコミット枈みのデヌタが芋えおしたうのでREAD UNCOMMITTEDなのでコミットしなくおも
芋えおしたうはずですが、怜玢条件に䞀臎しなくなり曎新できなくなったずいうこずかず。

確認TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0020 sec)

コミットTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0011 sec)

最終的な結果は、こちらですね。

 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0010 sec)
READ COMMITTED

READ COMMITTED。

トランザクション開始TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0011 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level read committed;
Query OK, 0 rows affected (0.0011 sec)

トランザクション開始TX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0006 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level read committed;
Query OK, 0 rows affected (0.0007 sec)

デヌタ読み取りTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0019 sec)

デヌタ読み蟌みTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0009 sec)

デヌタ曞き蟌みTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 1 row affected (0.0023 sec)

Rows matched: 1  Changed: 1  Warnings: 0

デヌタ曞き蟌みTX-B。こちらは、このたた埅ちになりたす。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0173 sec)

するず、TX-Bが進みたす。空振りになりたすが。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

Query OK, 0 rows affected (27.2145 sec)

Rows matched: 0  Changed: 0  Warnings: 0

こちらも他のトランザクションのコミット枈みのデヌタが芋えおしたうからですね。

コミットTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0009 sec)

最終結果はこちらですね。

 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0014 sec)
REPEATABLE READ

REPEATABLE READ。

トランザクション開始TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0009 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.0010 sec)

トランザクション開始TX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0007 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.0007 sec)

デヌタ読み取りTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0014 sec)

デヌタ読み取りTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0019 sec)

デヌタ曞き蟌みTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 1 row affected (0.0021 sec)

Rows matched: 1  Changed: 1  Warnings: 0

デヌタ曞き蟌みTX-B。そのたた埅ちになりたす。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0188 sec)

するず、TX-Bが進みたす。空振りになりたしたね 

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 0 rows affected (17.8448 sec)

Rows matched: 0  Changed: 0  Warnings: 0

REPEATABLE READだず、他のトランザクションのコミットしたデヌタは芋えないはずですが、どうなっおいるんでしょう。

コミットTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0015 sec)

最終結果。

 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0013 sec)

ちょっず気になるので、もう1床確認しおみたす。

トランザクション開始TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0007 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.0009 sec)

トランザクション開始TX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0009 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.0008 sec)

デヌタ読み取りTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0014 sec)

デヌタ読み取りTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0015 sec)

デヌタ曎新TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 1 row affected (0.0022 sec)

Rows matched: 1  Changed: 1  Warnings: 0

この時、TX-Bからデヌタを芋おみたす。

--TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0019 sec)

TX-Aの倉曎したデヌタは芋えおいたせん。

TX-Aをコミットしおみたす。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0147 sec)

もう1床TX-Bで読み蟌み。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0023 sec)

TX-Aのコミット前のデヌタが芋えおいたす。

デヌタ曞き蟌みTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 0 rows affected (0.0021 sec)

Rows matched: 0  Changed: 0  Warnings: 0

空振りたすね。

どうなっおいるんでしょう

ここで、select文にfor updateを付けおみたす。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1' for update;
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0015 sec)

なんず、他のトランザクションのコミット枈みのデヌタが芋えおしたいたした。

versionカラムも含めおみたしょう。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1' and version = 1 for update;
Empty set (0.0012 sec)



 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1' and version = 2 for update;
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0014 sec)

これず同じ挙動になったので、TX-Bのupdate文は空振りになったず考えるのがよさそうです。

コミットTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0011 sec)

最終結果。

 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0011 sec)
トランザクション分離レベルのダりングレヌド

こちらを芋るず、ロック付きの読み取りLocking Readではトランザクション分離レベルのダりングレヌドREAD COMMITTED盞圓に
なっおしたうが行われるようなのですが、今回の動きはこちらを再珟したこずになりそうですね。

MySQL/Postgres におけるトランザクション分離レベルと発生するアノマリーを整理する

ちょっず驚きたした 。

REPEATABLE READ WITH CONSISTENT SNAPSHOT

MySQLでは、トランザクション分離レベルがREPEATABLE READ WITH の時に限りWITH CONSISTENT SNAPSHOTを぀けるこずで
Snapshot Isolationず同等のトランザクション分離レベルにするこずができるようです。

 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction with consistent snapshot;
Query OK, 0 rows affected (0.0014 sec)

MySQL :: MySQL 8.4 Reference Manual :: 15.3.1 START TRANSACTION, COMMIT, and ROLLBACK Statements

ちょっず詊したみたのですが、今回の内容だずREPEATABLE READず動きに差がなかったので結果は割愛したす。

SERIALIZABLE

SERIALIABLE。

トラザクション開始TX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0010 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level serializable;
Query OK, 0 rows affected (0.0010 sec)

トランザクション開始TX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  SQL > start transaction;
Query OK, 0 rows affected (0.0011 sec)
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > set session transaction isolation level serializable;
Query OK, 0 rows affected (0.0003 sec)

デヌタ読み蟌みTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0013 sec)

デヌタ読み蟌みTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1';
+-----+---------+---------+
| id  | message | version |
+-----+---------+---------+
| id1 | Hello   |       1 |
+-----+---------+---------+
1 row in set (0.0016 sec)

デヌタ曞き蟌みTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 1 row affected (0.0023 sec)

Rows matched: 1  Changed: 1  Warnings: 0

デヌタ曞き蟌みTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0550 sec)

TX-Bは進み、空振りになりたす。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
Query OK, 0 rows affected (24.9590 sec)

Rows matched: 0  Changed: 0  Warnings: 0

コミットTX-B。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > commit;
Query OK, 0 rows affected (0.0014 sec)

最終結果。

 MySQL  localhost:3306 ssl  mydatabase  SQL > select * from mytable where id = 'id1';
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0019 sec)

REPEATABLE READず同じ動䜜になりたしたね。

ちなみに、SERIALIZABLEであっおもロック付きの読み蟌みだず他のトランザクションのコミット枈みのデヌタを読み取るこずができたす 。

-- TX-B
 MySQL  localhost:3306 ssl  mydatabase  ★  SQL > select * from mytable where id = 'id1' for update;
+-----+-----------------+---------+
| id  | message         | version |
+-----+-----------------+---------+
| id1 | Hello from TX-A |       2 |
+-----+-----------------+---------+
1 row in set (0.0012 sec)

PostgreSQL

最埌はPostgreSQLで。

READ UNCOMMITTED

READ UNCOMMITTED。ドキュメントを芋る限りREAD COMMITTEDず同じ動䜜のようですが、䞀応やっおおきたす。

トランザクション開始TX-A。

-- TX-A
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level read uncommitted;
SET

PostgreSQLの堎合、トランザクション内ではプロンプトに「*」ずいう衚瀺が付きたす。

トランザクション開始TX-B。

-- TX-B
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level read uncommitted;
SET

デヌタ読み取りTX-A。

-- TX-A
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ読み取りTX-B。

-- TX-B
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ曞き蟌みTX-A。

-- TX-A
mydatabase=*> update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 1

デヌタ曞き蟌みTX-B。ここでは埅ちになりたす。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
mydatabase=*> commit;
COMMIT

この時、TX-Bは空振りしたす。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 0

コミットTX-B。

-- TX-B
mydatabase=*> commit;
COMMIT

最終結果。

mydatabase=> select * from mytable where id = 'id1';
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)
READ COMMITTED

READ COMMITTED。

トランザクション開始TX-A。

-- TX-A
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level read committed;
SET

トランザクション開始TX-B。

-- TX-B
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level read committed;
SET

デヌタ読み取りTX-A。

-- TX-A
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ読み取りTX-B。

-- TX-B
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ曞き蟌みTX-A。

-- TX-A
mydatabase=*> update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 1

デヌタ曞き蟌みTX-B。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
mydatabase=*> commit;
COMMIT

TX-Bは動き出し、空振りしたす。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 0

コミットTX-B。

-- TX-B
mydatabase=*> commit;
COMMIT

最終結果。

mydatabase=> select * from mytable where id = 'id1';
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)

ここたでは、MySQLず同じですね。

REPEATABLE READ

REPEATABLE READ。

トランザクション開始TX-A。

-- TX-A
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level repeatable read;
SET

トランザクション開始TX-B。

-- TX-B
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level repeatable read;
SET

デヌタ読み取りTX-A。

-- TX-A
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ読み取りTX-B。

-- TX-B
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ曞き蟌みTX-A。

-- TX-A
mydatabase=*> update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 1

デヌタ曞き蟌みTX-B。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
mydatabase=*> commit;
COMMIT

するず、TX-B偎ぱラヌになりたす。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
ERROR:  could not serialize access due to concurrent update

PostgreSQLだずこういう挙動になっお矛盟を防ごうずするんですね。ある意味、デヌタベヌス偎で楜芳的排他制埡のようなこずを
やっおくれる印象を持ちたすね。

この状態になるず、TX-B偎はロヌルバックするしかありたせん。

-- TX-B
mydatabase=!> select * from mytable where id = 'id1';
ERROR:  current transaction is aborted, commands ignored until end of transaction block

ちなみに、コミットしおもロヌルバックするんですね。

-- TX-B
mydatabase=!> commit;
ROLLBACK

最終結果。

mydatabase=> select * from mytable where id = 'id1';
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)

ちなみに、MySQLず違っおロック付きで読み取りをしおも他のトランザクションがコミットしたデヌタが芋えるずいうこずはありたせん。

-- TX-B
mydatabase=*> select * from mytable where id = 'id1';
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)

mydatabase=*> select * from mytable where id = 'id1' for update;
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)
SERIALIZABLE

SERIALIZABLE。

トランザクション開始TX-A。

-- TX-A
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level serializable;
SET

トランザクション開始TX-B。

-- TX-B
mydatabase=> start transaction;
START TRANSACTION
mydatabase=*> set transaction isolation level serializable;
SET

デヌタ読み取りTX-A。

-- TX-A
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ読み取りTX-B。

-- TX-B
mydatabase=*> select * from mytable where id = 'id1';
 id  | message | version
-----+---------+---------
 id1 | Hello   |       1
(1 行)

デヌタ曞き蟌みTX-A。

-- TX-A
mydatabase=*> update mytable set message = concat(message, ' from TX-A'), version = version + 1 where id = 'id1' and version = 1;
UPDATE 1

デヌタ曞き蟌みTX-B。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;

埅機

コミットTX-A。

-- TX-A
mydatabase=*> commit;
COMMIT

TX-B偎は倱敗したす。

-- TX-B
mydatabase=*> update mytable set message = concat(message, ' from TX-B'), version = version + 1 where id = 'id1' and version = 1;
ERROR:  could not serialize access due to concurrent update

ロヌルバックTX-B。

-- TX-B
mydatabase=!> rollback;
ROLLBACK

最終結果。

mydatabase=> select * from mytable where id = 'id1';
 id  |     message     | version
-----+-----------------+---------
 id1 | Hello from TX-A |       2
(1 行)

おわりに

よくある楜芳的ロックをテヌマに、MySQLずPostgreSQLで各トランザクション分離レベルでどのような動䜜になるかを確認しおみたした。

MySQLのREPEATABLE READでのトランザクション分離レベルのダりングレヌドがなかなか衝撃的で、この分離レベルで期埅する
読み取り䞀貫性を突き砎っおいおかなり驚きたした。
䞀方で、PostgreSQLは曎新を止めおしたうんだな、ず。

よくあるRDBMSのうち、MySQLのみデフォルトがREPEATABLE READですが、䞀貫性が欲しいずころでロック付けた読み取りをしたりするず
むしろ䞀貫性を損なうこずになりそうなので気を぀けた方がいいんだなず思いたした。

いや、勉匷になりたしたね 。

アノマリヌの話もそうなのですが、デヌタベヌスに関する知識が浅いなず思ったのでもうちょっず勉匷しようず思いたす。