分散トランザクションに問題があります。
SQL Server 2008 R2、Windows 7、.Net 4.0 を使用しています。
これが私が実際にやりたいことです:
- バッファデータベース(Aという名前)があります
- 別のデータベース(Bという名前)があります
- WCF Webサービスを介してデータベースAからデータベースBにデータを送信したい(SOAP over HTTP、AとBは同じマシン上にない)
- データがデータベース B に正常に送信されると、データはデータベース A から削除されます
これは、私が現在抱えている問題の非常に単純化された説明であることに注意してください。私の実際のアプリケーションでは、複数の SQL Server インスタンス間でデータをレプリケートしていません (記事の最後にある更新を参照してください)。
すべてがトランザクションの一部であるため、操作全体がアトミックです。
これが私が現在シーケンシャルにやろうとしていることです(トランザクション内のすべて):
- データベースAからチャンクを読み取りました(たとえば50行)
- ステップ1で読み取った行を更新します(実際には、行のブール
Sent
列に値を設定true
したため、将来的にはこれらの行が送信されることがわかります) wsHttpBinding
トランザクション フローの一部である WCF Web サービス (SOAP 1.2、) を (クライアント側で) 消費します (同期呼び出しをブロックします)。Web サービス リクエストは、手順 1 で読み取ったデータを送信します。- Web サービスの実装 (サーバー側) は、データベース B にデータを挿入します (Web サービス要求に含まれるデータ)。
- サーバーから例外が受信されない場合、
Sent
値が asのデータtrue
はデータベース A から削除されます - 特定の値
FaultException
がサーバーから受信された場合、Sent
値が true のデータがデータベース A から削除されます - その他
FaultExceptions
およびその他の例外 (エンドポイントが見つからないなど) の場合、Sent
値が asのデータtrue
はデータベース A から削除されません
- サーバーから例外が受信されない場合、
注: 実際には複数のバッファ データベース (A1、A2、A3...、AN) と複数のターゲット データベース (B1、.... BN) があります。N 個のデータベースを処理するための N 個のスレッドがあります。
サーバーとクライアントを実行すると、すべて正常に動作します。データは、チャンクごとにデータベース A からデータベース B にアトミックに「転送」されます。トランザクションの途中でクライアントを残酷に停止すると (プロセスが強制終了されます)、ほとんどの場合、すべて問題ありません (つまり、データがデータベース A から削除されたり、追加されたりすることはありません)。データベース B) に。
しかし、ときどき (これが私の実際の問題です)、データベース B がロックされることがあります。ロックを解除する唯一のオプションは、サーバー プロセスを終了することです。
問題が発生すると、アクティブなトランザクションがあることを MSDTC で確認できます (現在、3 つのバッファー データベースと 3 つのターゲット データベースを扱っているため、スクリーンショットには 3 つのトランザクションがあることに注意してください。 3DBあります)。
データベース B を再度実行しようとすると、データベース B がロックされていることがSELECT
わかります。下の図でわかるように、私の要求は、サーバー プロセスによってデータベース B にSELECT
対応するセッション ID 67 によってブロックされます。INSERT
サーバー プロセスが終了するまで、ロックは永久に残ります (1 時間以上経ってもトランザクション タイムアウトは発生しません)。MSDTC でトランザクションを検証またはキャンセルできません。「it cannot be aborted because it is not "In Doubt"
」という警告が表示されます。
データベース B がロックされたままになっているのはなぜですか? クライアントが終了した場合、トランザクションが失敗し、タイムアウト後にデータベース B のロックが解放されるべきではありませんか?
サーバー側のコードは次のとおりです。
// Service interface
[ServiceContract]
public interface IService
{
[OperationContract]
[FaultContract(typeof(MyClass))]
[TransactionFlow(TransactionFlowOption.Allowed)]
void SendData(DataClass data);
}
// Service implementation
[ServiceBehavior()]
public partial class Service : IService
{
[OperationBehavior(TransactionScopeRequired = true)]
public void SendData(DataClass data)
{
if (data == null)
{
throw new FaultException<MyClass>(new MyClass());
}
try
{
// Inserts data in database B
using (DBContextManagement ctx = new DBContextManagement())
{
// Inserts data using Entity Framework
// This will add some entities to the context
// then call context.SaveChanges()
ctx.InsertData(data);
}
}
catch (Exception ex)
{
throw new FaultException<MyClass>(new MyClass());
}
}
}
これが私のサーバー側の構成です(セルフホステッドWCFサービス):
<system.serviceModel>
<services>
<service name="XXXX.MyService">
<endpoint binding="wsHttpBinding" bindingConfiguration="WsHttpBinding_IMyService" contract="XXXX.IMyService" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="WsHttpBinding_IMyService" transactionFlow="true" allowCookies="true" >
<readerQuotas maxDepth="32" maxArrayLength="2147483647" maxStringContentLength="2147483647" />
<security mode="None" />
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="">
<serviceTimeouts transactionTimeout="00:00:20" />
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<dataContractSerializer maxItemsInObjectGraph="2147483647" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
これが私のクライアントコードです:
try
{
using (DBContextManagement ctx = new DBContextManagement())
{
using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted, Timeout = new TimeSpan(0, 0, 30) }, EnterpriseServicesInteropOption.None))
{
// First of all, retrieves data from database A
// Internally queries the database A through Entity Framework
var data = ctx.GetData();
// Mark data as sent to the server
// Internally updates the database A through Entity Framework
// This actually set the Sent property as true, then call
// context.SaveChanges()
ctx.SetDataAsSent(data);
try
{
// Send data to the server
MyServiceClient proxy = new MyServiceClient();
MyServiceClient.SendData(data);
// If we're here, then data has successfully been sent
// This internally removes sent data (i.e. data with
// property Sent as true) from database A through entity framework
// (entities removed then context.SaveChanges)
ctx.RemoveSentData();
}
catch (FaultException<MyClass> soapError)
{
// SOAP exception received
// We internally remove sent data (i.e. data with
// property Sent as true) from database A through entity framework
// (entities removed then context.SaveChanges)
ctx.RemoveSentData();
}
catch (Exception ex)
{
// Some logging here
return;
}
ts.Complete();
}
}
}
catch (Exception ex)
{
// Some logging here (removed from code)
throw;
}
これが私のクライアント構成です:
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WsHttpBinding_IMyService"
closeTimeout="00:01:00"
openTimeout="00:01:00"
receiveTimeout="00:10:00"
sendTimeout="00:01:00"
allowCookies="false"
bypassProxyOnLocal="false"
hostNameComparisonMode="StrongWildcard"
transactionFlow="true"
maxBufferPoolSize="2147483647"
maxReceivedMessageSize="2147483647"
messageEncoding="Text"
textEncoding="utf-8"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32"
maxStringContentLength="2147483647"
maxArrayLength="16384"
maxBytesPerRead="4096"
maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8080/MyService.svc"
binding="wsHttpBinding"
bindingConfiguration="WsHttpBinding_IMyService"
contract="XXX.IMyService"
name="WsHttpBinding_IMyService" />
</client>
</system.serviceModel>
だから私の質問は:
- 分散トランザクションを使用したパターン/設計は有効で堅牢な設計ですか?
- クライアントのトランザクションが強制終了された場合、データベース B が無期限にロックされる原因は何ですか?
- 期待どおりに動作させるには、何を変更 (構成および/またはコードおよび/または設計) する必要がありますか?
ありがとう。
編集
実際のユースケースの説明を単純化しすぎたと思います。複数の SQL Server インスタンス間で単純なデータ レプリケーションを実際に行っているようです。それはよく説明されていません (私の悪い点です)、それは私が達成しようとしていることではありません。
私は単にデータを複製しているわけではありません。マシン M1 で A から読み取り、マシン M2 で B に書き込みますが、書き込まれるのは読み取ったものではなく、読み取ったものから計算された値です。SQL Server レプリケーション サービスがビジネス計算を処理できると確信していますが、いくつかの理由でこの方法を実行することはできません。
- 私は実際にはサーバー側 (データベース B に書き込む) を担当していないため、SQL Server レプリケーションを使用できません。反対側に SQL Server があるかどうかさえわかりません (MySQL、PostgreSQL などを使用した Java バックオフィスである可能性があります)。
- 同じ理由で、SQL Server サービス ブローカーまたはメッセージ指向ミドルウェア (IMO に適合) を使用できません (異機種データベースおよび環境の可能性があります)。
- インターフェイスが定義および修正されたため、Web サービスの内容を変更することはできません。
私は WCF で立ち往生しており、相互運用性の要件のために、MSMQ などを使用するようにバインド構成を変更することさえできません。MSMQ は確かに優れています (プロジェクトの別の部分で既に使用しています) が、これは Windows のみです。SOAP 1.2 は標準プロトコルであり、SOAP 1.2 トランザクションも標準です (WS-Atomic
実装)。
実際、WCF トランザクションは良い考えではないかもしれません。
実際にどのように機能するかを正しく理解していれば(間違っている場合は修正してください)、サーバー側でトランザクションスコープを「継続」するだけで済みます。これには、サーバー側でトランザクションコーディネーターを構成する必要があり、おそらく壊れます私の相互運用性のニーズ (繰り返しますが、トランザクション コーディネーターとうまく統合されていないサーバー側のデータベースが存在する可能性があります)。