来自不同线程/进程的SQL冲突,并发UPDATE和SELECT

时间:2017-10-19 20:42:42

标签: c# multithreading tsql sql-server-2014

我的团队正在努力解决有关并发线程/进程的奇怪的Microsoft SQL Server问题。很少,在两个不同的线程上UPDATE和同时SELECT之间似乎存在竞争条件。很难再现,因为时机至关重要。

发生冲突时,SELECT不会返回所需的记录,但不会生成错误。相反,SELECT表现得好像记录不存在,这是不正确的。结果我们的服务变得混乱,并且在我们恢复之前会有一些混乱。

为了消除我们服务中出现错误的可能性,我使用普通的C#代码和System.Data.SqlClient重现了这个问题。我很乐意根据要求提供该代码。我试图将代码和表格减少到重现问题所需的最低限度。

如下所示创建测试表:

CREATE TABLE [TestTable] 
(
    [RecordId] int IDENTITY(1,1) UNIQUE,
    [Locked] int NOT NULL DEFAULT 0,
    [Priority] int NULL,
    [Status] int NULL,
    [SystemName] nvarchar(50) NULL
)

该表有一个像这样创建的索引:

CREATE NONCLUSTERED INDEX [IDX_GENERAL] 
ON [TestTable] ([Status], [SystemName], [Locked]) 
INCLUDE ([RecordId], [Priority])

该表包含如下所示的单个记录:

INSERT INTO [TestTable] ([Locked], [Priority], [Status], [SystemName])
VALUES (0, 3, 3000, 'System1')

在我们的服务中,多个线程和进程通过原子设置他们的“锁定”来获取对记录的独占写入权限。领域到一个。然后,在进行任何必要的修改之后,线程/进程必须通过重置其“锁定”记录来释放记录。列为零。我看到的是一个线程解锁记录时非常罕见的情况,同时另一个线程试图找到它:

一个线程执行这样的UPDATE(成功):

UPDATE [TestTable] 
SET [Locked] = 0 
OUTPUT INSERTED.* 
WHERE [Locked] = 1 
  AND [RecordId] = 1 
  AND [Status] = 3000 
  AND [SystemName] = 'System1'

同时,另一个线程像这样执行SELECT(返回空):

SELECT [RecordId] 
FROM [TestTable] 
WHERE [Status] = 3000 
  AND [SystemName] = 'System1' 
ORDER BY Priority DESC, RecordId ASC

我认为索引是问题的一部分,因为如果我删除了StatusSystemName键,那么我就无法重现该问题。我不知道会导致这种行为的原因。我读过的所有内容都说这根本不可能发生。

我欢迎任何有关如何排除故障的问题,想法或建议......

1 个答案:

答案 0 :(得分:0)

你的逻辑存在缺陷。在开始时,许多线程尝试获取锁的记录的id。

 SELECT [RecordId] 
 FROM [TestTable] 
 WHERE [Locked]=0 AND [Status]=3000 AND [SystemName]='System1' 
 ORDER BY Priority DESC, RecordId ASC

然后他们尝试设置锁定

UPDATE [TestTable] 
SET [Locked]=1 
OUTPUT INSERTED.* 
WHERE [Locked]=0 AND [RecordId]={0} 
      AND [Status]=3000 AND [SystemName]='System1'

当然可能会发生

  1. 第一个主题获取未锁定记录的ID
  2. 第二个主题获取未锁定记录的ID
  3. 第二个线程尝试锁定记录并成功
  4. 第一个线程尝试锁定记录并失败
  5. 这是a race condition bug

      

    竞争条件是程序结果发生的错误   取决于两个或多个线程中的哪一个到达特定块   代码第一。多次运行程序会产生不同的结果,   并且无法预测任何给定运行的结果。

    在你的情况下,这不是一个错误,而是一些罕见的逻辑案例 你有两个选择

    1. 打开两个查询的公共事务。 (跟DB锁问好)
    2. 考虑到执行时第二个查询记录可能已被另一个线程锁定。 (我会选择这个选项)