ディスカッション (8件)
データベースの並行処理で発生する「レースコンディション(競合状態)」は、実行タイミングがわずかにズレるだけで発生しなくなるため、再現が非常に困難な厄介なバグです。この記事では、Postgresにおいて「同期バリア(Synchronization Barriers)」を活用し、複数のスレッドやプロセスの実行を特定のポイントで意図的に待ち合わせることで、競合状態を確実にシミュレーションしてテストする高度なデバッグ手法について解説します。
それPostgreSQLの問題じゃなくて、君のコードの問題だよ。
正直言って、あんな書き方は絶対にするべきじゃない。普通にこう書くか、
UPDATE employees
SET salary = salary + 500
WHERE employee_id = 101;
もっと複雑な処理ならSTORED PROCEDUREを使えばいい。JS側で全部トランザクション処理するなら、わざわざデータベースを使う意味がないよ。
2つ(あるいはN個)のタスク間で、PostgreSQLの操作のあらゆるインターリービングを試すバージョンがあれば面白そう。https://crates.io/crates/loom は、同期プリミティブを使うRustコードに対して似たようなことをやってるね。
この記事に関連して、Postgresのadvisory locks (pg_advisory_xact_lock) も便利だよ。行レベルじゃなくて論理レベルでの競合が起きる場合に使える。例えば、ある型の「最初の」リソースを作ろうとするリクエストが2つ同時に来た時、SELECT FOR UPDATEすべき行がまだ存在しない場合とかね。
advisory lockを使えば、ダミー行や専用のロックテーブルなしで、任意のキー(エンティティタイプと親IDのハッシュなど)で直列化できる。トランザクション終了時に自動解放されるから後片付けも不要。
記事で紹介されていたバリアテストの手法もここでうまく機能するはず。advisory lockの獲得と後続のINSERTの間にバリアを差し込んで、2つ目のトランザクションが1つ目がコミットされるまでブロックされることを確認すればいい。
JavaScriptエンジニアがトランザクションとSQLの幼稚園レベルの基礎を学習中か。笑えるな。「プログラマーに学位なんて不要」っていうキャンプの連中か?
PostgresにはSERIALIZABLEというトランザクション分離レベルがあるよ。これを使えば競合条件なんて気にする必要は一切なくなる。
もし何らかの理由で使いたくないなら、記事にあるような「バリア」や「フック」を使ったテスト手法は実質役に立たない。それって潜在的な競合条件をあらかじめ把握している必要があるけど、わかっているなら最初から避けるコードを書くはずだよね。本当に恐ろしいのは、気づきにくい競合条件の方だよ。
それを見つけるには、トランザクションステップの異なるインターリービングを何度も繰り返すランダムテストを使うべきだ。個々のDBクエリ呼び出しに直接フックするフレームワークを作れば、わざわざテスト用の「フック」を追加する必要なんてなくなる。
とはいえ、DBクエリ単体の中でも競合条件が発生する可能性はあるから、それだけで全てのバグを見つけられるわけじゃない。
素直にSERIALIZABLEを使って、無駄な苦労やテストを書く時間を省いたほうがいいよ。
この記事の内容なんて、これで十分だよ:
トランザクション用のテーブル(ordersみたいな名前で)を作る。
そこにINSERTトリガーを設定する。
トリガー内で "update … set balance += amount where accoundId = id" という単一の更新を行う。これならDBエンジン自体がアトミックに処理してくれる。
さらに残高用のcheck制約で >= 0 を設定すれば、何千もの支払いが同時に発生してもマイナスになることはない。もしマイナスになればエラーになるし、INSERTトリガーが再スローすればINSERT自体が実行されない。あとはバックエンド側でそれをキャッチすればいい。
――
これだけ。INSERTトリガーとcheck制約。明示的なロックもSTORED PROCEDUREも、バックエンド側のロックも何もいらない。単純なINSERTだけ。負荷や同時ユーザー数に関係なく魔法のように動くし、爆速だよ。
だからDBにはACIDがあるんだ。
――
宣伝:道具を使いこなそう。Postgresql/Mssql/その他をただのバックエンドエンジニアとして扱うな。DBはテキストファイルじゃないんだよ。
こういう競合を避けるために、俺はいつも専用のロックテーブルにユニークキーでロック用の行を挿入して、数分間のタイマーをかけてる。トランザクションが完了したらその行を削除するんだ。こうすれば、同時にアクセスしてきたユーザーは最初の一つしかロックを取れず、他のリクエストは失敗する。もしタイマーが切れたら、トランザクションが完了しなかったと見なして数分後に再試行させる仕組みにしてるよ。