Dirty Read の概念を学んだので、動作を試すのに Go の sql パッケージを使って検証スクリプトを書いてみる。
こういうのは手を動かしてみると記憶に残る。
compose.yaml はこちら。
services: # MySQL mydb: image: mysql:8.0 platform: linux/x86_64 container_name: mydb environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: mydb MYSQL_USER: user MYSQL_PASSWORD: password TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --general_log=1 volumes: - ./docker/db/data:/var/lib/mysql ports: - 3306:3306
go.mod はこちら。
module example go 1.20 require github.com/go-sql-driver/mysql v1.7.1
使用するテーブルはこちら。 サンプルデータも先に入れておく。
DROP TABLE IF EXISTS "tbl"; CREATE TABLE tbl ( id MEDIUMINT NOT NULL AUTO_INCREMENT, name CHAR(30) NOT NULL, counter TINYINT NOT NULL, PRIMARY KEY (id) ); INSERT INTO tbl (name, counter) VALUES ('データ1', 10);
プログラムを書いていく。
DBをOpenしてコネクションを生成する。
トランザクション分離レベルをセッション単位でセットするために、コネクションを明示的に生成する。
以降のコードは conn
を Dirty Read を発生させるコネクション、 conn2
を書き込みを実行するコネクションとする。
(エラーハンドリングはすべて panic
で済ませている。こちらは、「書き殴りのコードなので落ちた箇所さえわかれば良い」という考えの適当なハンドリングである)
ctx := context.Background() db, err := sql.Open("mysql", (&mysql.Config{ User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:3306", DBName: "mydb", ParseTime: true, }).FormatDSN()) if err != nil { panic(err) } conn, err := db.Conn(ctx) if err != nil { panic(err) } conn2, err := db.Conn(ctx) if err != nil { panic(err) }
Dirty Read を発生させたいセッションのみ READ UNCOMMITTED にしておく。
_, err = conn.QueryContext(ctx, "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") if err != nil { panic(err) }
それぞれのセッションでトランザクションを開始する。
tx, err := conn.BeginTx(ctx, nil) if err != nil { panic(err) } tx2, err := conn2.BeginTx(ctx, nil) if err != nil { panic(err) }
書き込み用のトランザクションで number
カラムを 2 に更新する。
_, err = tx2.Query("UPDATE tbl SET counter = 2 WHERE id = 1") if err != nil { panic(err) }
書き込み対象となったレコードの number
カラムを参照する。Dirty Read となり 2 が入ってくる。
var counter int err = tx.QueryRow("SELECT counter FROM tbl WHERE id = 1").Scan(&counter) if err != nil { panic(err) } fmt.Println("counter: ", counter)
実行後にデータを初期状態に直すのが面倒なのでロールバックしておく。
tx.Rollback() tx2.Rollback()
実行していく。
docker compose up -d go run main.go counter: 2
想定通り 2 が参照できている。
業務ではトランザクション分離レベルをいじったことは一度もない。
Dirty Read が発生するのは READ UNCOMMITTED のみなので、Dirty Read に遭遇したこともなければ考慮したこともなかった。
が、RDB を触るプログラマーとして頭に入れておくべきだよなーと思って検証してみた。Non-repeatable Read、Phantom Read の検証も同じように行って記事にする予定。