我偶然发现了一个非常好奇的案子.我们有一个SQL Server 2012数据库和这样的表
CREATE TABLE [dbo].[ActiveTransactions]
(
[Id] [BIGINT] IDENTITY(1,1) NOT NULL,
[Amount] [DECIMAL](12, 4) NOT NULL,
[TypeId] [SMALLINT] NOT NULL,
[GameProviderId] [SMALLINT] NULL,
[UserId] [INT] NOT NULL,
[Checksum] [NVARCHAR](150) NOT NULL,
[Date] [DATETIME2](7) NOT NULL,
[ExternalKey] [VARCHAR](60) NULL,
[ExternalDescription] [NVARCHAR](1000) NULL,
[OperatorId] [SMALLINT] NULL,
[GameId] [NVARCHAR](50) NULL
)
这个表有多个索引,但我想在这里讨论的两个是PK_ActiveTransactions(主键,聚集),它们是:
ALTER TABLE [dbo].[ActiveTransactions]
ADD CONSTRAINT [PK_ActiveTransactions]
PRIMARY KEY CLUSTERED ([Id] DESC)
和IX_ActiveTransactions_UserIdAmount(非聚集,非唯一):
CREATE NONCLUSTERED INDEX [IX_ActiveTransactions_UserIdAmount]
ON [dbo].[ActiveTransactions] ([UserId] ASC, [Id] DESC)
INCLUDE ([Amount])
有一个查询依赖于我的解决方案的主要部分,并在启动某个进程时调用.基本上每次在我的代码端调用SomeMethod时,它都会启动SQL事务,然后执行过程(如下所示),从而锁定它选择的条目,然后计算一些东西并在该表中插入新行并提交事务.锁定过程执行此SQL语句
SELECT TOP 1
id ,
Amount ,
TypeId ,
GameProviderId ,
UserId ,
[Checksum] ,
[Date] ,
ExternalKey
FROM ActiveTransactions WITH ( UPDLOCK )
WHERE @UserId = UserId
ORDER BY Id DESC
现在就是这样的.当我查看此表中的某些条目时,似乎有多个(同时请求)条目为同一个@UserId选择了相同的条目.确切地说,有5个新条目(当时要求,因为它们具有在代码侧计算的相同的[日期]值),它们都选择了相同的条目,然后重新计算了一些东西(所有5个都计算了相同的事情)并同时插入5个新行,而不是一个接一个地执行(这应该是由SELECT查询结束时的WITH(UPDLOCK)语句引起的,我相信).
然后我尝试做了这样的事情,我打开了三个新的查询窗口,我在一个窗口中用BEGIN TRAN命令启动了一个事务然后在SELECT语句上面执行,在其他两个窗口中我做了同样的事情,当我提交了第一个语句时查询在此之后获得它,在提交第二个语句后,第三个获取它. (一切都按预期工作),在查询结束时添加WITH INDEX(INDEX_NAME)(UPDLOCK仍然存在)之后,事情开始变得奇怪了.到第一个选择我指定WITH INDEX(PK_ActiveTransactions)(主键)和另外两个我指定WITH INDEX(IX_ActiveTransactions_UserIdAmount).运行所有这三个命令之后,加上第一个命令在同一个表中的INSERT,(第二个和第三个仍在等待第一个命令完成)但是当我提交第一个命令时,第二个获取旧条目和第三个命令同时获得了新的条目.我认为这种行为可能导致上面解释的错误,但这怎么可能?
SQL Server会同时为同一个查询使用两个不同的执行计划(因此使用不同的索引)吗?这个表在一天结束时到达了大约10-15百万个条目,但每天早上大约在早上6点执行作业,这使得表只有1-2百万行.这会导致SQL Server意外切换索引吗?但无论如何我认为这是一个系列问题,这意味着即使提交后,索引也可能不包含其中的已提交数据.
上面的问题只发生了几次,我能够识别它们发生两次
最佳答案 您需要检查表和索引上正在获取的锁(请参阅下面的链接). SQL Server能够对索引和数据进行单独锁定.默认情况下,它不会锁定所有索引.
注意:下面是猜测.
查询#1从未在IX_ActiveTransactions_UserIdAmount上获取锁定,因此查询#2能够搜索索引并获取锁定,然后等待释放行数据锁以完成其操作.释放此锁后,查询#2会抓取并保留它,同时执行其他代码.同时,查询#3仍在等待数据和索引锁定.一旦查询#2完成并且所有锁被释放,那么只有查询#3能够使用索引进行搜索并因此搜索最新数据.
综上所述:
查询#1和查询#2都能够并行搜索表并返回相同的行.查询#2必须等待查询#1完成才能获得更新锁.由于查询#1实际上并不修改最后一行而是插入新行,因此不会为了查询#2而更改索引.
有关问题反转的讨论,请参阅https://www.mssqltips.com/sqlservertip/1485/using-sql-server-indexes-to-bypass-locks/.
附加评论:
我认为它会更可靠并且可能为您的目的提供更好的性能来锁定特定用户ID的“用户”表(如果存在),而不是依赖索引锁定正常工作.