$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

MySQLのREPEATABLE READとREAD COMMITTEDの違いを知るために色々試した

MySQLのトランザクション分離レベルについてふんわりとした理解しかないなと感じた。もう少し理解するために、とくにREPEATABLE READとREAD COMMITTEDの違いを手を動かして色々確認してみた。

以下の記事を参考にした。

大まかな違い

公式ドキュメントを見る限り

  • ノンリピータブルリード、ファントムリードが発生するか
  • 範囲に含まれるギャップへのほかのセッションによる挿入をブロックするか

の違いがありそうに見える。

ノンリピータブルリード、ファントムリードが発生するかを試す

以下のテーブルを作る。

CREATE TABLE `posts` (
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

REPEATABLE READの場合

まずデフォルトのREPEATABLE READから。

# トランザクション1
begin;

# トランザクション2
begin;

# トランザクション1
insert into posts (title, body) values('title1', 'body1');

# トランザクション2
# ダーティリードが発生していない
select * from posts;
Empty set (0.01 sec)
select count(*) from posts;
+----------+
| count(*) |
+----------+
|        0 |
+----------+

# トランザクション1
commit;

# トランザクション2
# ノンリピータブルリードが発生していない
select * from posts;
Empty set (0.01 sec)
# ファントムリードが発生していない
select count(*) from posts;
+----------+
| count(*) |
+----------+
|        0 |
+----------+

# トランザクション2
rollback;
# トランザクション1のCOMMITが見えるようになる
select * from posts;
+--------+-------+
| title  | body  |
+--------+-------+
| title1 | body1 |
+--------+-------+
select count(*) from posts;
+----------+
| count(*) |
+----------+
|        1 |
+----------+

ノンリピータブルリードもファントムリードも発生していないことが確認できる。REPEATABLE READの説明ではファントムリードは起こると書いていることが多いが実装次第であり、MySQLにおいてはファントムリードも起こらなくなっている。

READ COMMITTEDの場合

# トランザクション2
set session transaction isolation level read committed;
begin;

# トランザクション1
begin;
insert into posts (title, body) values('title1', 'body1');

# トランザクション2
# ダーティリードが発生していない
select * from posts;
Empty set (0.01 sec)

# トランザクション1
commit;

# トランザクション2
# ノンリピータブルリードが発生した
> select * from posts;
+--------+-------+
| title  | body  |
+--------+-------+
| title1 | body1 |
+--------+-------+

ちゃんとノンリピータブルリードが起こることが確認できる。

範囲に含まれるギャップへのほかのセッションによる挿入をブロックするか

続いて、ロック範囲についても検証する。テーブル構造をこのようにする。

CREATE TABLE `posts` (
  `id` int NOT NULL,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

両方ともREPEATABLE READ

> select * from posts;
+----+--------+-------+
| id | title  | body  |
+----+--------+-------+
|  1 | title1 | body1 |
|  2 | title2 | body2 |
+----+--------+-------+

# トランザクション1
begin;

# トランザクション2
begin;

# トランザクション1
UPDATE posts SET title = 'title2updated' WHERE id >= 2 and id < 5;

# トランザクション2
insert into posts (id, title, body) values (10, 'title10', 'body10');
# -> lockされる

特定範囲をUPDATEかけた場合、他のトランザクションからギャップ範囲に対してINSERTをしたときにロックされることが確認できた。

UPDATE側がREPEATABLE READ, INSERT側がREAD COMMITTED

# トランザクション1
set session transaction isolation level read committed;
begin;

# トランザクション2
begin;
UPDATE posts SET title = 'title2updated' WHERE id >= 2 AND id < 5;

# トランザクション1
insert into posts (id, title, body) values (10, 'title10', 'body10');
# -> lockされる

UPDATE側がREPEATABLE READなら、INSERT側がREAD COMMITTEDだとしてもロックされることが確認できた。

UPDATE側がREAD COMMITTED, INSERT側がREPEATABLE READ

# トランザクション1
set session transaction isolation level read committed;
begin;

# トランザクション2
begin;

# トランザクション1
UPDATE posts SET title = 'title2updated' WHERE id >= 2 AND id < 5;

# トランザクション2
insert into posts (id, title, body) values (10, 'title10', 'body10');
# -> lockされない

UPDATE側がREAD COMMITTEDなら、INSERT側がロックされないことを確認できた。

まとめ

MySQLにおいてはREPEATABLE READとREAD COMMITTEDの違いは大まかに以下がありそうだ。

  • ノンリピータブルリード、ファントムリードが発生するか
  • 範囲に含まれるギャップへのほかのセッションによる挿入をブロックするか

実際に手を動かして試したところ、

  • ノンリピータブルリード・ファントムリードは、REPEATABLE READでは発生せず、READ COMMITTEDでは発生する
  • REPEATABLE READではUPDATEやDELETEの範囲のギャップロックがかかるのでほかセッションによるギャップ範囲への挿入はブロックされる。READ COMMITTEDではギャップロックがかからない

ということを確認できた。

Goで関数の引数に、union型っぽくstruct Aもしくはstruct Bのどちらかを受け取れるようにしたい

Goで関数の引数に、struct Aという型もしくはstruct Bのどちらかを受け取るということをしたかった。interfaceをちゃんと切ってそれに必要なメソッドをAとBに実装することで実現できることを知った上で、あまり丁寧にそういうことをせずにやりたい。

色々調べると、genericsを使うとできるようだ。

package main

import "fmt"

type A struct {
    Field1 int
}

type B struct {
    Field2 string
}

type AorB interface {
    A | B
}

func PrintAorB[T AorB](s T) {
    // Tで受け取ったものをそのままs.(type)とは出来ないので、一旦anyへキャスト
    switch v := any(s).(type) {
    case A:
        fmt.Println(v.Field1)
    case B:
        fmt.Println(v.Field2)
    }
}

PrintAorB(A{Field1: 1})   // -> 1
PrintAorB(B{Field2: "2"}) // -> 2
// PrintAorB(2)           // -> type error

こんな感じにすることでAかBだけを受け付ける引数を実現できた。

またこのやり方で1つ便利に使えそうなユースケースとして、いろんなところで大量に使われている関数があったとして引数を新しい型に変更したい時に、いったん両方の型を受け付けた上で少しずつ変更していき、全部置き換わったら新しい型だけに変更するみたいな手法が取れそうに思った。もちろん関数自体を分けるという手もあるが、こういうやり方もあるんだなと覚えておく。

参考

本の内容が頭に入ってくるのは結局は知見まとめノートを作っている時

最近は読書のやり方を変えてみたら知識の吸収速度・引き出し速度が上がった話 - $shibayu36->blog;に書いているやり方で読書をしている。こういう流れだ。

  • (1)学びたいと思った知識が書いてありそうな本を2~5冊選ぶ
  • (2)1冊ずつざっくり読みながら、面白かった部分・気になった部分はKindleで黄色にハイライトしておく
  • (3)全冊読み終わったら、ハイライトした部分だけ眺めて、やっぱりおもしろいと思ったところは赤のハイライトを付け直す
  • (4)赤のハイライトを眺めて、読書ノートに転記する
  • (5)とくにおもしろい部分については、自分の知見まとめノートにカテゴリごとに整理する

しばらくこれを続けて感じたのは、結局のところ(4)〜(5)に至るまで書籍の内容が全然頭に入っていないということだ。(4)(5)の時に、はじめて「書いている内容が言いたかったのはこういうことだったのか」と頭が急に理解したり、「この知識は今の仕事のこの部分に役立ちそうだ」と急にアイデアが出たりする。読んでる段階でも自分では理解できていると感じているが、その段階だけで急に理解できたと感じる瞬間はあまりなく、振り返ってみるとまったく理解できてないことに気づく1

人によって理解のフレームワークはまったく異なると思うが、自分にとっては「自分の言葉でまとめる」というフェーズを経ないと理解ができないようだ。このフェーズを必ずやると読める冊数は少なくなってしまい知識をなかなか身につけられないと焦る気持ちもあるのだが、結局読んだとしてもあんまり頭に残ってなかったら意味ないので、今後も続けていこうと思う。


  1. ただし自分が経験上分かっているが言語化できていないことについては、読んだだけで理解できることがある