トランザクションの注意点

アイキャッチ画像

データベースを利用するシステム開発では、複数のSQLをまとめて処理するために「トランザクション」を利用します。
トランザクションを適切に設計することで、処理途中でエラーが発生した場合でもデータの不整合を防ぎやすくなります。

一方で、トランザクションは「処理をまとめて、成功したらCOMMIT、失敗したらROLLBACKすればよい」という単純なものではありません。
トランザクションの範囲や処理時間、同時実行、ロックなどを適切に考えなければ、性能劣化やデッドロック、データ不整合などの問題につながることがあります。
今回は、SQLやデータベースを扱うSEが知っておきたい、トランザクションの注意点について解説します。

そもそもトランザクションとは?

トランザクションとは、データベースに対する複数の処理を「ひとまとまりの処理」として扱う仕組みです。

例えば、銀行口座間の送金を考えてみましょう。
Aさんの口座から1万円を引き出し、Bさんの口座に1万円を入金するとします。

このとき、

  • Aさんの残高を1万円減らす
  • Bさんの残高を1万円増やす

という2つの処理が必要です。

もし1の処理だけ成功して2の処理に失敗すると、1万円がどこにも存在しない状態になってしまいます。
そこで、2つの処理を1つのトランザクションとして扱い、すべて成功した場合に確定(コミット)、途中で問題が発生した場合には取り消し(ロールバック)を行います。

ACID特性を理解する

トランザクションを理解するうえで重要なのが「ACID特性」です。

Atomicity(原子性)
処理を「すべて成功」または「すべて失敗」として扱う性質です。
先ほどの送金処理であれば、引き出しだけ成功して入金に失敗する、といった中途半端な状態を防ぎます。

Consistency(整合性)
トランザクションの前後で、データベースが定められた整合性を保つ性質です。
例えば、外部キー制約や一意制約などのルールを守った状態を維持します。

Isolation(分離性)
複数のトランザクションが同時に実行されても、それぞれの処理が適切に分離される性質です。
分離レベルの設定によって、他のトランザクションの変更をどのタイミングで参照できるかなどが変わります。

Durability(永続性)
コミットされたデータが、障害などが発生しても失われにくい状態で保持される性質です。
この4つは、トランザクション設計を考える際の基本となります。

トランザクションを長時間保持しない

実務で特に注意したいのが、トランザクションを長時間保持しないことです。
トランザクション中にデータを更新すると、データベースでは他の処理との競合を避けるためにロックが発生する場合があります。

例えば、
「データベースを更新する」

「トランザクションを開始したまま外部APIを呼び出す」

「APIの応答を待つ」

「その後コミットする」
という設計にすると、外部APIの応答待ちの間もトランザクションが継続します。

処理時間が長くなれば、その分だけロックが長く保持され、他の処理が待たされる可能性があります。
そのため、トランザクションの範囲は必要以上に広げないことが重要です。

外部処理をトランザクション内に入れすぎない

前述の内容とも関連しますが、外部APIやファイル操作などをデータベースのトランザクション内に含める場合は注意が必要です。
データベースのロールバックによって、外部APIの処理やファイル操作まで自動的に元に戻るとは限りません。

例えば、

  • DBの注文情報を更新
  • 外部サービスに注文情報を送信
  • DB更新でエラーが発生してロールバック

という処理を考えます。

DB側は元に戻っても、外部サービスへの送信が取り消されるとは限りません。
このように、トランザクションの境界をどこに設定するかは、システム全体の整合性を考えて設計する必要があります。

デッドロックに注意する

トランザクションを利用する際に避けたい問題の一つが「デッドロック」です。

デッドロックとは、複数のトランザクションがお互いにロックの解放を待ち続け、処理が進まなくなる状態です。

例えば、

  • トランザクションAがテーブル1をロックする
  • トランザクションBがテーブル2をロックする
  • Aがテーブル2のロックを取得しようとする
  • Bがテーブル1のロックを取得しようとする

という状況になると、お互いに相手の処理を待つ状態になります。

対策としては、テーブルやレコードをロックする順序を統一することなどが考えられます。

また、デッドロックが発生した場合に備えて、アプリケーション側でリトライを行う設計が必要になるケースもあります。

トランザクションの範囲を明確にする

「とりあえず処理全体をトランザクションで囲む」という設計は避けたほうがよいでしょう。

トランザクションの範囲が広くなるほど、ロック時間が長くなったり、他の処理との競合が発生したりする可能性があります。

一方で、範囲を狭くしすぎると、本来ひとまとまりで処理すべきデータが別々にコミットされ、整合性が崩れる可能性があります。

重要なのは、

「この処理は、どこまで成功したら一つの処理として成立するのか?」

という観点からトランザクションの境界を決めることです。

例外発生時のロールバックを確認する


実装時には、正常系だけでなく異常系についても確認する必要があります。
例えば、

  • DB更新中に例外が発生した場合
  • 複数回の更新処理の途中でエラーになった場合
  • タイムアウトが発生した場合
  • デッドロックが発生した場合

などです。

特に、使用しているフレームワークによっては、例外の種類によってロールバックされる条件が異なる場合があります。
「エラーになったから当然ロールバックされる」と決めつけず、利用している言語・フレームワーク・DBの仕様を確認しておくことが大切です。

まとめ

トランザクションは、システムのデータ整合性を守るために欠かせない仕組みです。
一方で、トランザクションの範囲を広げすぎたり、ロックを長時間保持したりすると、性能低下やデッドロックなどの問題につながる可能性があります。

SEとしては「トランザクションを使うこと」だけを考えるのではなく、どこまでを一つの処理として扱うべきなのかを意識して設計することが重要です。
また、DBだけではなく、外部APIや他システムとの連携まで含めて整合性を考えることで、より安定したシステム設計につながります。

これからトランザクションを扱う方は、まずACID特性とコミット・ロールバックの基本を押さえ、そのうえでロックやデッドロック、並行実行について理解を深めていくとよいでしょう。