我遇到了分布式事务的问题。
我正在使用 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
列设置为 valuetrue
,所以将来我知道这些行已发送) - 我使用(客户端)一个 WCF Web 服务(SOAP 1.2,
wsHttpBinding
),它是事务流(阻塞同步调用)的一部分。webservice请求发送步骤1中读取的数据 - webservice 实现(服务器端)在数据库 B 中插入数据(包含在 webservice 请求中的数据)
- 如果没有从服务器接收到异常,则从数据库 A 中删除
Sent
值为 as的数据true
FaultException
如果从服务器接收到特定Sent
值,则从数据库 A 中删除值为 true 的数据- 对于其他
FaultExceptions
和其他异常(未找到端点或其他任何情况),Sent
值为 astrue
的数据不会从数据库 A 中删除
- 如果没有从服务器接收到异常,则从数据库 A 中删除
注意:实际上有多个缓冲区数据库(A1、A2、A3...、AN)和多个目标数据库(B1、...BN)。有 N 个线程来处理 N 个数据库。
如果我运行我的服务器和客户端,一切正常。数据是从数据库 A 到数据库 B 的每个块原子地“传输”的。当我在事务中间粗暴地停止我的客户端(进程被杀死)时,大多数时候一切都很好(即数据没有从数据库 A 中删除或添加到数据库 B)。
但有时(这是我的实际问题),我的数据库 B 被锁定。解锁它的唯一选择是终止服务器进程。
当问题发生时,我可以在 MSDTC 中看到有一个活动事务(请注意,在屏幕截图中有 3 个事务,因为我目前正在处理 3 个缓冲区数据库和 3 个目标数据库,但有时即使只有 1 个事务有 3 个数据库)。
我看到数据库 B 在尝试再次运行数据库 B 时被锁定SELECT
。如下图所示,我的SELECT
请求被会话 ID 67 阻止,该会话 ID 对应于INSERT
服务器进程在数据库 B 中的一个。
锁定永远保持(没有事务超时,即使在 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 被无限期锁定?
- 我应该更改什么(配置和/或代码和/或设计)以使其按预期工作(即,如果我的客户端进程死亡/崩溃,我希望事务被中止,以便数据库 B 不被锁定)?
谢谢。
编辑
我确实认为我过度简化了对实际用例的描述。看起来我实际上是在多个 SQL Server 实例之间进行简单的数据复制。这没有很好的解释(我的错),这不是我想要达到的目标。
我不是简单地复制数据。我从机器 M1 上的 A 读取,然后写入机器 M2 上的 B,但是写入的不是我读过的,而是来自读取的一些计算值。我很确定 SQL Server 复制服务可以为我处理业务计算,但由于某些原因我不能这样做:
- 我不能使用 SQL Server 复制,因为我实际上不负责服务器端(写入数据库 B)。我什至不确定另一边是否会有 SQL Server(可能是带有 MySQL、PostgreSQL 或任何东西的 Java 后台)
- 出于同样的原因(可能是异构数据库和环境),我不能使用 SQL Server 服务代理或任何面向消息的中间件(适合 IMO)
- 我无法更改 Web 服务,因为现在已经定义并修复了接口。
我被 WCF 困住了,由于互操作性要求,我什至无法更改绑定配置以使用 MSMQ 或其他任何东西。MSMQ 肯定很棒(我已经在项目的另一部分使用它),但它只是 Windows。SOAP 1.2 是标准协议,SOAP 1.2 事务也是标准的(WS-Atomic
实现)。
实际上,也许 WCF 事务不是一个好主意。
如果我正确理解了它的实际工作原理(如果我错了,请纠正我),它只允许在服务器端“继续”事务范围,这将需要在服务器端配置事务协调器,这可能会中断我的互操作性需求(同样,服务器端可能有一个与事务协调器没有很好集成的数据库)。