取引ガイドライン

この文書では、アプリケーション・コードにおけるデータベース・トランザクションの使用例をいくつか示します。

より詳しい情報は、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 の各ステートメントは以前の一貫性のある状態にロールバックします。

note
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

データベースは、参照されているissueproject レコードの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つのステートメントはトランザクションの一部ではなく、何か問題が発生してもロールバックされません。これらはサードパーティ呼び出しとして機能します。