トランザクションとマルチスレッドデータベースアクセス

最近、次のコードを実行する必要がありました(最も単純化された形式で表示)。



public void Start() { using (var transactionScope = new TransactionScope()) { ... GetOrCreateCompany(someValue); ... transactionScope.Complete(); } } private Company GetOrCreateCompany(string companyName) { var company = _companiesRepository.GetCompany(companyName); //     ;     -  null if (company == null) company = _companiesRepository.Add(companyName); return company; }
      
      







このコードはマルチスレッド環境で実行され、各入力ストリームはStartメソッドを受け取りました(つまり、各ストリームには独自のトランザクションがありました)。



この一見単純なコードにはいくつかのニュアンスがありますが、これについては後述します。







タスクには一般的な解決策があります。必要な制約を設定し、トランザクションをループで囲み、例外が発生した場合は再試行します(試行回数を超えた場合は例外をスローします)。 ただし、競合するロックの結果として多数のスレッドが存在する場合、このアプローチの動作は非常に遅くなります(動作しないと言う方が簡単です)。 したがって、私はより具体的なソリューションを実装し始めました。



以下は私の最初のソリューションのコードです(将来、トランザクションとロックの詳細はMySQLを例として使用することを検討します):



 public void Start() { using (var transactionScope = new TransactionScope(TransactionScopeOption.Requires, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable })) { ... GetOrCreateCompany(someValue); ... transactionScope.Complete(); } } private Company GetOrCreateCompany(string companyName) { var company = _companiesRepository.GetCompanyWithWriteLock(companyName); //    SELECT ... FOR UPDATE if (company == null) company = _companiesRepository.Add(companyName); return company; }
      
      







上記のコードでは、会社の選択は対応する範囲と共にブロックされ(Serializableの使用により)、コミット後にのみロック解除されます。 同じ会社を読み取ろうとする次のトランザクションは、ブロックされたトランザクションのコミットを待機します。



一般に、このソリューションは機能します。 しかし、 アムダールの法則 、コード、そしてアムダールの法則を見ると、悲しいことになります。トランザクションがコミットされるまでレコードをブロックする必要があるだけでなく(ロック/ロックの下のコードの量が修正されない)、選択が目的の会社を返す場合でも、書き込みロックを設定します(つまり、追加する必要はありません)。 最初の段落がこの決定の基礎であり、そこから抜け出すことができない場合、2番目の段落との闘いは続きます。



次の2つの場合、会社を選択してもnullは返されません。

  1. 会社は、すでにコミットされた以前のトランザクションの一部として追加されました。
  2. 会社は現在のトランザクションの一部として追加され、現在のトランザクションをコミットした後にのみ他のトランザクションから見えるようになります
最初のケースを個別に処理してみましょう。ブロックする前に、現在のトランザクションの外部でリクエストを実行し、すでにコミットされているトランザクションの中に検索するデータがあるかどうかを確認します。



 private Company GetOrCreateCompany(string companyName) { Company company; //    ,  ,       .   -    ,      using (var independentTransactionScope = new TransactionScope(TransactionScopeOption.Suppress)) { company = _companiesRepository.GetCompany(companyName); } //      ,        -     if (company != null) return; //company   null   : 1.      ,        2.      var company = _companiesRepository.GetCompanyWithWriteLock(companyName); if (company == null) //       -       company = _companiesRepository.Add(companyName); } }
      
      







この最適化の小さなマイナス点は、現在のトランザクションの外部で値が表示されない場合(つまり、選択に対して返された独立したTransactionScope null内の場合)、選択を複製する必要があることです。 しかし、生産性の向上を背景に、この余分なサンプルに目を向けることができます。



なぜこの記事を書いたのですか? 私は間違った方向に進んだと感じています。問題は些細なことであり、解決策は...そうではありません。 さらに、この問題の解決策の例を見つけることができなかったため、さらに困惑しています。 コメンテーターが代替ソリューションを提案するか、私の意見に賛成することで状況を明確にすることを望みます。



更新する

個人的な通信で、Shaddix haberman (同時に、私の同僚)は、独立したトランザクションを使用するというアイデアの興味深い展開を提案しました-独立したトランザクションを読むだけでなく追加することです。 これは、一見、小さな変更ですべてが大きく変わります。



最初の実装:



 public class TransactionCode { private static readonly object _lockObject = new object(); public void Start() { using (var transactionScope = new TransactionScope()) { ... GetOrCreateCompany(someValue); ... transactionScope.Complete(); } } private Company GetOrCreateCompany(string companyName) { Company company; //,        using (var indepdentTransactionScope = new TransactionScope(TransactionScopeOption.RequiresNew)) { var company = _companiesRepository.GetCompany(companyName); } if (company != null) return; //   ,     //    using (var independentTransactionScope = new TransactionScope(TransactionScopeOption.RequiresNew)) { var company = _companiesRepository.GetCompanyWithWriteLock(companyName); if (company == null) company = _companiesRepository.Add(companyName); independentTransactionScope.Complete(); } } }
      
      







ここで、会社をメイントランザクションに追加するのではなく、これらの目的のために独立したトランザクションを使用します。 ここでの主な機能は、すぐにコミットされる独立したトランザクションで追加が行われるため、追加された値がすぐに(前のソリューションのようにメイントランザクションがコミットされた後ではなく)他のトランザクションから見えるようになることです。 まあ、このアプローチがより効果的であることは間違いありません。私の場合、パフォーマンスの向上は300%でした(そして、データがより動的になればなるほど、ロックの寿命が短くなるため、より大きくなります)。

しかし、このソリューションには欠点もあります。

1.主な欠点は、メイントランザクションがロールバックされた場合、独立したトランザクションで追加されたデータがロールバックされないことです(コミットしました)。 一般的な場合、欠点は非常に重要です...しかし私にとってはそうではありません:GetOrCreateのようなメソッドでは、比較的独立したデータを追加しますが、それは遅かれ早かれ追加されます:このトランザクションではなく、次のトランザクションで。

2. TransactionScopeOption.RequiresNewおよびTransactionScopeOption.Suppressで非読み取り専用トランザクションを使用することには個人的な偏見があります(これらについては次の記事で説明します)。



したがって、私は後者の解決策がより好きであるという事実にもかかわらず、一般的なケースでは、それらの一方が他方より優れているとは言えません-それらは異なっています。



All Articles