使用 MSMQ 和 SQL 服务器的分布式事务,但有时会出现脏读

Distributed transaction with MSMQ and SQL Server but sometimes getting dirty reads

我们的 SQL Server 2014 数据库设置为 READ_COMMITTED_SNAPSHOT

我们使用 MSMQ 和分布式事务(我们使用 MassTransit 2.10)

在我们系统的一部分中,我们从队列中读取一条消息,进行数据库更新,然后将一条新消息发布到队列中(所有这些都在一个事务中)。

我们发现了一种情况,在处理下一条消息时似乎没有提交更新(它从相同的 table 读取第一部分更新),尽管我希望该消息只在更新数据库的同时在队列中。当我们稍后查询 table 时,更新的数据如预期的那样存在。这似乎只在我们有高负载时才会发生,而且很少发生。

我们代码的简化版本

// 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
}    

所以这是我的理解:

处理范围后,将提交对 table X 的更改以及将消息发布到队列

MethodThatReadsFromTableX() 从 table X 读取时,我希望更改在那里(会话不应在第一个完成之前创建,因为它无法获取来自队列的消息)

我的预期是否正确?可能是什么问题?

以下是我对这个问题的看法。

在分布式事务中,通常使用两阶段提交。在第一阶段,交易协调员要求每个人准备提交。如果每个人都同意,第二阶段协调员将命令每个人提交更改。请注意,当节点在此阶段提交更改时 - 它不会再回滚。另请注意,参与者 A(例如 Sql 服务器)和参与者 B (MSMQ) 实际提交更改之间可能存在不可避免的差距。当 MSMQ 已经提交更改时,可能是 SQL 服务器尚未提交(尽管已被命令提交)。

因此,当 MSMQ 提交事务时,没有什么可以阻止将创建的消息传送到您的处理器,正如您所说,它在另一个线程中运行。因此,即使在您离开第一个 "scope" 块之前 - 您的处理器可能会开始处理收到的消息,并且 SQL 服务器 可能 仍未提交更改。分布式事务不能阻止这种行为,因为它不能以某种方式强制每个参与者在完全相同的时间完成提交他们的更改。

长话短说 - 我认为它按预期工作,您应该准备好在处理 MSMQ 消息和实施重试逻辑时可能会丢失所需的数据。

我会给你一个非常非常简短的答案。

使用 Serializable 作为您的隔离级别。只有这样才能保证读者不会被作者屏蔽。

其余...

SQL 非常擅长行级锁定,除非您按顺序插入聚集索引。在这种情况下,插入和处理重复键错误会带来性能优势——一个数量级的优势。

您还需要确保可能进行脏读的读者也在使用可序列化进行读取——否则,您将遇到在提交数据库事务之前消耗已发布消息的情况.我在大批量生产系统中看到过这种情况,并且发生了(当然,使用 RabbitMQ 而不是 MSMQ - RabbitMQ 的调度比 MSMQ 快得多)。