使用MSMQ和SQL Server的分布式事务,但有时会出现脏读取问题。

3
我们的SQL Server 2014数据库设置为READ_COMMITTED_SNAPSHOT。
我们使用MSMQ和分布式事务(我们使用MassTransit 2.10)。
在系统的某个部分,我们从队列中读取消息,对数据库进行更新,然后发布新消息到队列中(所有这些都在单个事务下完成)。
我们发现一种情况,即当处理下一条消息时,似乎更新没有被提交(它从第一部分更新相同的表),尽管我期望该消息仅在更新数据库时在队列上。当我们稍后查询该表时,更新的数据就像预期的那样出现了。这只会在高负载和非常罕见的情况下发生。
我们代码的简化版本
// code that processes message 1
using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions() { Timeout = TimeSpan.FromMinutes(30), IsolationLevel =  IsolationLevel.ReadCommitted }) 
{
     MethodThatUpdatesTableX();
     MethodThatCreatesMessage2();
     scope.Complete();
}    

// message picked up from MSMQ and then (this runs in different thread):
// code that process message 2
using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew,  new TransactionOptions() { Timeout = TimeSpan.FromMinutes(30), IsolationLevel = IsolationLevel.ReadCommitted }) 
{
     MethodThatReadsFromTableX(); // so here it seems that changes made in MethodThatUpdatesTableX is sometimes (though rarely) not read
    // other stuff
}    

我这里的理解是:

当作用域被释放时,对表X所做的更改将被提交,并且消息将发布到队列中。

MethodThatReadsFromTableX()从表X中读取数据时,我期望更改已经存在(会话不应在第一个会话完成之前创建,因为它无法从队列中获取消息)

我的期望是正确的吗?可能出了什么问题?

2个回答

2
以下是我对这个问题的想法。
在分布式事务中,通常使用两阶段提交。在第一阶段,事务协调器要求每个参与者准备提交。如果每个参与者都同意,在第二阶段,协调器将命令每个参与者提交更改。注意,在此阶段节点提交更改后,它将不再回滚。还要注意的是,参与者A(例如Sql Server)和参与者B(MSMQ)实际上提交它们的更改可能存在不可避免的差距。当MSMQ已经提交更改时,可能是SQL Server尚未提交(尽管已经被命令提交)。
因此,当MSMQ提交事务时,没有任何阻止创建的消息传递到您的处理器,正如您所说,运行在另一个线程中。因此,在您离开第一个“scope”块之前,您的处理器可能会开始处理接收到的消息,并且SQL Server可能仍然没有提交更改。分布式事务无法防止这种行为,因为它无法强制每个参与者在完全相同的时间内完成提交其更改。
长话短说 - 我认为它的工作方式符合预期,您应该准备好在处理MSMQ消息时可能缺少所需数据,并实现重试逻辑。

1
我会给你一个非常简短的答案。
使用Serializable作为您的隔离级别。这是确保读者会被写者阻塞的唯一方法。
其余的...
SQL在行级锁定方面非常出色,除非您正在按顺序插入到聚集索引中。在这种情况下,插入并处理重复键错误会带来性能上的好处--数量级的好处。
您还需要确保可能获得脏读取的读者也使用可序列化进行阅读--否则,您将遇到一个已发布的消息在数据库事务提交之前被消耗的情况。我在高容量生产系统中看到过这种情况,并且它发生了(当然,使用RabbitMQ而不是MSMQ - RabbitMQ比MSMQ更快地分派)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接