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个新条目(当时要求,因为它们具有在代码侧计算的相同的[Date]
值),它们都选择了相同的条目然后重新计算了一些东西(所有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意外切换索引吗?但无论如何我认为这是一个系列问题,这意味着即使提交后,索引也可能不包含其中的已提交数据。
上述问题只发生了几次,我能够识别它们发生了两次
答案 0 :(得分:1)
您需要检查表和索引上正在获取的锁(请参阅下面的链接)。 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,而不是依赖索引锁定正常工作。