blog

トランザクションの分離と開発への応用

はじめに\n\n問題点\n\n開発には一連のビジネスロジックがあります:\n// 1. データベースに問い合わせを行い、レコードが存在するかどうかを確認します。\n// 2. レコードが存在しない場合...

Jul 27, 2020 · 5 min. read
シェア

はじめに

問題点

現在、そのようなビジネスロジックのセットが開発中です:

// 1. データベースにレコードが存在するか照会る
// 2. 存在しない場合は、HTTPで別のサービスを呼び出し、一連の操作を行い、データを挿入する。
// 3. 再度確認し、存在すれば次の操作に進む。
@Transactional
public void foo(){
 // # 1st query
 EntityA a = aService.findOneBy();
 if(a == null){
 // call other service and insert data
 httpU.st("tp://:/eA");
 // # 2nd query
 a = aService.findOneBy();
 }
}

注: この側の設計にはいくつかの問題があるかもしれませんが、リモートコールサービスメソッドcreateA()は、実際には、直接挿入されたデータのJSONの戻り値は、クエリデータベースを繰り返す必要はありませんが、createA()このメソッドは、デフォルトでは、いくつかの理由を変更するようにすることはできません返されます。

一見すると、このコードには何の問題もないように見えますし、ロジックも単純です。しかし実際には、2 つのクエリの結果は同じであり、 createA() が正常に実行された後に新しいデータを取得することはできません。これはなぜでしょうか?具体的な理由はデータベースの分離レベルに影響されます。

トランザクションの分離と@Transactional

つまり、複数のトランザクションが同時に実行されている場合、各トランザクションが行った変更は他のトランザクションから分離されていなければなりません。また、トランザクション間の操作が互いに見えないことも意味します。一方、データベースの性能と信頼性を天秤にかけるために、標準SQLでは集中分離レベルとしてREAD UNCOMMITED、READ COMMITED、REPEATABLE READ、SERIALIZABLEを与えています。

  • RAED UNCOMMITED: クエリ文を使用するとロックされず、コミットされていない行を読み取ることができます;
  • READ COMMITED:レコードにのみレコード・ロックを追加し、レコード間にギャップ・ロックを追加しないため、ロックされたレコードの近くに新しいレコードを挿入することができます;
  • REPEATABLE READ:同じ範囲のデータを複数回読み取ると、最初のクエリのスナップショットが返され、異なるデータ行は返されませんが、ファントムリードが発生する可能性があります;
  • SERIALIZABLE: InnoDBは全てのクエリ文に暗黙的に共有ロックを追加し、ファントム・リードの問題を解決します;

MySQLのデフォルトのトランザクション分離レベルはREPEATABLE READですが、Next-Keyロックによってファントム・リードをある程度解決することができます。

MySQL の分離レベルは以下の SQL で確認できます:

use performance_schema;
select * from global_variables where variable_name = 'tx_isolation';

Springにおけるアサーティブ・トランザクション・アノテーションの使用法を詳しく見てみましょう。Transactionalでは以下のパラメータを設定できます:

name設定ファイルに TransactionManager が複数ある場合、このプロパティを使用して、選択するトランザクション・マネージャを指定できます。
伝播デフォルト値はREQUIREDです。
アイソレーショントランザクションの分離レベル。デフォルト値は DEFAULT です。
timeout制限時間を超えてもトランザクションが完了していない場合、トランザクションは自動的にロールバックされます。
read-onlyトランザクションが読み取り専用かどうかを指定します。デフォルト値はfalseです。データの読み取りなど、トランザクションを必要としないメソッドを無視するには、read-onlyをtrueに設定します。
rollback-forトランザクションのロールバックを引き起こす例外の種類を指定するために使用します。
ノーロールバックトランザクションをロールバックせずに、no-rollback-for で指定された例外タイプをスローします。

isolation = DEFAULTは、Springがデータで設定された分離レベル(REPEATABLE READ)をデフォルトとすることを意味し、MySQLはファントム・リードも解決します。そのため、どちらのクエリ操作でもヌルレコードが返されます。これはトランザクションにとっては明らかに正常な結果ですが、ビジネスロジックにとっては問題があります。上記のコードは、他のトランザクションによって行われた変更を「ファントムリード」しようとしています。

ファントムリーダブル

一部のデータが再現不可能でファントムリードに見えることが明らかな場合、それは本当に問題ではありません。上記のコードが正しく動作するためには、上記のような変更が可能です。

  1. foo() のトランザクション・アノテーション構成を直接変更します。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void foo(){}

これはMySQLデータベースのREPEATABLE READの分離レベルを1つ落とすことに相当します。 繰り返し不可能な読み取りやファントム・リードの問題はREAD_COMMITTEDレベルで発生するため、他のトランザクションによってデータベースに加えられた変更を読み取ることができ、問題は解決します。

  1. メソッドを非トランザクションとして実行するか、別のトランザクションとして実行します。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public findOneBy(){}

非トランザクション方式で実行すると、更新されたデータを他のトランザクションから読み込むことができます。

@Transactional(propagation = Propagation.REQUIRES_NEW)
public findOneBy(){}

Propagation.REQUIRES_NEW このアイデアは、新しいトランザクションを作成し、現在のトランザクションが存在する場合はそれをハングアップさせるというものです。 1st query --> createA --> 2nd query この方法は、2つの照会操作をfoo()のトランザクションに追加せず、それぞれのトランザクションで別々に実行し、トランザクションを手作業でシリアライズする、つまり逐次的に実行することと同じです。

まとめ

ほとんどのシナリオでは、@Transactional を Service Tier のメソッドに直接追加することで、メソッドをトランザクションとして実行することができます。しかし、特定のシナリオでは、この記事で説明するシナリオのように、トランザクションの分離レベルと伝播の振る舞いを細かいレベルで手動で制御する必要があります。

分離レベルが高いほど、データベースの一貫性が強くなりますが、パフォーマンスが低下します。この点は、ダーティ・リード、反復不可能なリード、ファントムリードという共通の問題のトランザクションの開発において調整する必要があるでしょう。

別の質問がありますが、私は答えを得ることができるかどうかわからない:SQLの標準は、分離とトランザクションを定義し、トランザクションは、他のトランザクションの操作の結果を読み取ることはできませんが、もしt1がデータと計算の一部を読んで、その後、t2が変更され、コミットし、t1の計算は、問題のすべての結果が発生することはありません?非繰り返しリードの意義は何ですか?外部トランザクションからの更新を読み取ることができる場合の問題は何ですか?

Read next

もし、DeFiの哲学が主張するように

本当に非中央集権的であれば、一度システムにバグが発生しても、それを取り消してやり直すことはできませんし、利用者の利益も保証されませんが、中央集権的な金融システムであれば、こうした問題は明確に解決されますし、利用者も補償を受けることができます。現時点でDeFiがそれほど「分散型」でなければ、利用者の利益を保証することはできないでしょう。

Jul 27, 2020 · 2 min read