悲観もあれば楽観もある「トランザクション」の常識:企業システムの常識をJBossで身につける(8)(1/4 ページ)
企業向けアプリケーションのさまざまな“常識”をJavaのオープンソース・フレームワーク群である「JBoss」から学んでいきましょう。企業システムを構築するうえでの基礎となる知識をリファレンス感覚で説明していきます。初心者から中堅、ベテランまで大歓迎!
ありえない! 企業システムでの不完全なデータ
企業活動の日々の業務で発生するさまざまなデータ。それらを保存・利用するために、企業はITへの投資を行い、システムを構築しています。そうしたシステムにおいて、データを保存する際に防がなければならないのが不完全な状態での登録や更新です。
また、昨今のシステムでは保存先が複数存在するケースが多々あり、各保存先間での整合性を保証する必要があります。そこでシステム開発の際に重要となるのが、今回のテーマである「トランザクション」という考えです。
今回は、トランザクションを通して、それらに関連するJava標準の技術やJBossのフレームワークについて説明していきたいと思います。
いまさら聞けない「トランザクション」とは
ある複数の処理をひとまとまりとして行うことを、1つの「トランザクション」と呼びます。昨今のコンピュータシステムにおいては、データベースに保存することが多いですが、更新する対象のテーブルが複数あったとしても、それを一連の処理として行うなら、そのトランザクションは1つとして数えます。
上記のような一連の処理を保証するためにトランザクションは、下記の4つの特性を有していなければなりません。
Atomicity (原子性) |
トランザクションにおいて、すべてが処理されるかまったく処理されないかのどちらかであること。一部のみ処理されるという状態は不可 |
---|---|
Consistency (一貫性) |
トランザクションを処理する前の状態と処理した後の状態でデータの整合性が取れていること |
Isolation (分離性) |
トランザクションの処理途中の状態は、コミットされるまでほかのトランザクションからは見えてはいけない。また、ほかのトランザクションから影響を受けてもいけない |
Durability (永続性) |
トランザクションが終了した後の状態は、障害などがおきたとしても、その状態に変化はないこと |
表1 ACID特性 |
これらの頭文字をとって「ACID特性」と呼びます。このACID特性を表す例として、銀行口座の振り込み処理がよく引き合いに出されます。仮に、口座Aから口座Bへ10万円を振り込む(口座Aから10万円を引き、口座Bへ10万円を加算する)というオペレーションを想定します(図1)。
先ほどの特性を踏まえて、期待する正しい処理を表現すると、下記のようになります。
- 口座Aから口座Bへの振り込みが一連の処理として行われる:「原子性」
- 口座Aから引かれた金額と口座Bに振り込まれた金額が一致する:「一貫性」
- 口座Aから引かれる処理がほかの振り込み処理により侵されない:「分離性」
- 口座Aと口座Bへの処理結果と、次回の開始時とで状態が同じである:「永続性」
しかし、ACID特性を持たない場合は、下記のような状況が起こり得るということです。
- 口座Aから10万円が引かれたが、口座Bに10万円を加算する処理がされなかった
- 口座Aから10万円が引かれたが、口座Bに100万円が加算されてしまった
- 口座Aから10万円が引かれた際、同時にほかのトランザクションで口座Aから5万円が引かれた。合計15万円引かれているはずが、5万円しか口座Aから引かれていなかった
- 口座Aから10万円が引かれ、口座Bに10万円が加算され処理が終了したが、新たに振り込みを行おうとしたときに前回の振り込み前の状態(口座A:100万円、口座B:0円)に戻っていた
このような事態を避けるために、トランザクション処理は非常に重要です。
「分散トランザクション」と「2フェイズコミット」
上記のような口座間の振り込みにおいて、仮に保存先が1つのデータベースである(口座Aと口座Bが同じデータベース上に存在する)場合は、トランザクション処理も比較的容易になります。データベースには、トランザクションの機能が存在するため、1つのデータベースで完結していればトランザクションの制御は容易です。
しかし、複数のデータベースに分かれている場合はどうでしょうか。この場合、複数のデータベースのすべての処理を終了するまでが1つのトランザクションとなり、そのすべてが正常に終了するか、処理されずにすべてが元の状態に戻る必要があります。
データベースの持つトランザクションの制御は、1つのデータベースにしか及ばないため、複数存在する場合は、別の方法でトランザクション全体を保証する必要があります。このようなケースのトランザクションを「分散トランザクション」と呼びます(それに対して、普通のトランザクションは「ローカルトランザクション」と呼ばれます)。
この分散トランザクションを処理するための解決方法として「2フェイズコミット」があります。もう一度、口座間の振り込みを例に説明します。
2フェイズコミットでは、まず仮の処理を行います。口座A、口座Bへの処理(処理1、処理2)を行い、それぞれから成功か失敗かを確認します。この処理は一時的な状態で保持されています。
次に確定処理を行います。口座A、口座Bの両方から成功と返ってくればそのまま一時処理を本処理として確定、どちらかでも失敗したと返ってきたなら一時処理を破棄します。こうすることで全体の原子性を保証します。
楽観もあれば悲観もある「排他制御(ロック)」とは
これまで説明してきたトランザクション処理に密接にかかわってくるのが、「排他制御」という概念です。共有可能なリソースの整合性を確保するためには、複数からのアクセスがあった場合、1つのアクセスに独占させてほかからは利用できないようにする必要があります。データベースを例にすると、レコードの参照・挿入・更新・削除処理に際してロックを取り、ほかのプロセスからの参照・変更を防ぐということです。
排他制御は一般的に大きく2つに分けられます。「楽観ロック」と「悲観ロック」です。
楽観ロック
あるプロセスがデータを更新している間に、ほかのプロセスからも処理が可能です。しかし、ほかのプロセスからの更新処理は失敗します。このロックは更新処理をする段階で取得されます。
楽観ロックの場合、複数のユーザーが同じデータを見ることができます。しかし、ほかのユーザーが情報を更新した場合、更新したという通知を受けることはありません。つまり、現在見ているデータが最新であるかどうかは分からないということです。
悲観ロック
あるプロセスのデータを更新が終了するまで、ほかのプロセスは処理ができない。このロックはデータを参照した時点から始まります
悲観ロックの場合、あるユーザーが参照・更新処理を行っている間は、ほかのユーザーは参照も更新もできません。これであれば、いま見ているデータが最新であることは保証されます。しかし、次に処理を行いたい人は先にロックを取得したユーザーの処理が終了するまで待たされることになります。
2つのロックは一長一短
このように2つのロックにはそれぞれ一長一短があります。上記の説明を見ると、排他制御の本来の目的を考えれば悲観ロックの方が良いと思われるかもしれません。しかし、多人数がアクセスすることが前提であれば、これでは運用が成り立たないケースが出るでしょう。
データベースにおけるロックは、行に対する悲観ロックがデフォルトとなっています(Oracleを例に取ると、SELECT 〜FOR UPDATE)。楽観ロックは、データに対して何かを行うわけではなく、アプリケーションやHibernateのようなフレームワークでの制御で保証します。
トランザクションに関する説明は以上です。大まかにはつかめたのではないかと思います。次ページでは、もう少し具体的にJavaでのトランザクションの実現方法を見ていくことにしましょう。
Copyright © ITmedia, Inc. All Rights Reserved.