取引ガイドライン
この文書では、アプリケーション・コードにおけるデータベース・トランザクションの使用例をいくつか示します。
より詳しい情報は、PostgreSQLのトランザクションに関するドキュメントを参照してください。
データベースの分割とシャーディング
ポッドグループは、メインの GitLab データベースを分割し、データベーステーブルの一部を他のデータベースサーバーに移動する予定です。
私たちはまずci_*
関連のデータベーステーブルの分解を始めます。現在のアプリケーション開発経験をメンテナーするために、コードベースにツールと静的アナライザーを追加し、正しいデータアクセスとデータ変更方法を保証します。データベーストランザクションの定義に正しい形式を使用することで、将来のリファクタリング作業を大幅に削減できます。
トランザクションブロック
ActiveRecord
ライブラリは、データベース文をトランザクションにグループ化する便利な方法を提供します:
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: 'updated title')
project.update!(last_update_at: Time.now)
end
このトランザクションには 2 つのデータベース・テーブルが含まれます。エラーが発生した場合、UPDATE
の各ステートメントは以前の一貫性のある状態にロールバックします。
ActiveRecord::Base
クラスの参照は避け、代わりにApplicationRecord
を使用してください。トランザクションとデータベースロック
トランザクション・ブロックがオープンされると、データベースはリソースに対して必要なロックの取得を試みます。ロックの種類は、実際のデータベース・ステートメントによって異なります。
以下のコードが2つの異なるプロセスから同時に実行される同時更新シナリオを考えてみましょう:
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: 'updated title')
project.update!(last_update_at: Time.now)
end
データベースは、参照されているissue
とproject
レコードのFOR UPDATE
ロックを取得しようとします。この場合、これらのロックに対して2つのトランザクションが競合し、そのうちの1つだけがロック取得に成功します。もう一方のトランザクションは、最初のトランザクションが終了するまでロックキューで待機しなければなりません。2番目のトランザクションの実行はこの時点でブロックされます。
トランザクション速度
ロックの競合を防ぎ、安定したアプリケーションのパフォーマンスを維持するためには、トランザクションブロックは可能な限り速く終了する必要があります。トランザクションはロックを取得すると、トランザクションが終了するまでロックを保持します。
アプリケーションのパフォーマンスとは別に、長時間実行するトランザクションはデータベースのマイグレーションをブロックすることで、アプリケーションのアップグレード処理にも影響を与える可能性があります。
危険な例: サードパーティの API 呼び出し
次の例を考えてみましょう:
member = Member.find(5)
Member.transaction do
member.update!(notification_email_sent: true)
member.send_notification_email
end
ここでは、send_notification_email
メソッドが成功した send_notification_email
ときだけnotification_email_sent
列が更新されるようにします。send_notification_email
この send_notification_email
メソッドは、電子メール送信サービスへのネットワーク要求を実行します。基礎となるインフラストラクチャがタイムアウトを指定しなかったり、ネットワーク呼び出しに時間がかかりすぎたりすると、データベース・トランザクションは開いたままになります。
トランザクションにはデータベース・ステートメントのみを含めるのが理想的です。
transaction
ブロックでの処理は避けてください:
- 以下のような外部ネットワークからのリクエスト:
- Sidekiqジョブのトリガー。
- メールの送信
- HTTP APIコール
- 別の接続を使用してデータベース文を実行します。
- ファイルシステムのオペレーション。
- 長時間のCPU集約型計算
-
sleep(n)
の呼び出し。
明示的なモデル参照
トランザクションが同じデータベーステーブルのレコードを変更する場合、Model.transaction
ブロックを使用することをお勧めします:
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
Ci::Build.transaction do
build_1.touch
build_2.touch
end
上記のトランザクションでは、transaction
ブロックのモデルと同じデータベース接続をトランザクションに使用しています。マルチデータベース環境では、次の例は危険です:
# `ci_builds` table is located on another database
class Ci::Build < CiDatabase
end
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
ApplicationRecord.transaction do
build_1.touch
build_2.touch
end
ApplicationRecord
クラスはCi::Build
レコードとは異なるデータベース接続を使用します。トランザクションブロック内の2つのステートメントはトランザクションの一部ではなく、何か問題が発生してもロールバックされません。これらはサードパーティ呼び出しとして機能します。