为非平凡的 where 子句更新死锁
Upsert Deadlock for non-trivial where clause
我正在尝试编写一些 SQL 来根据相当复杂的条件进行更新:
BEGIN TRAN;
UPDATE LocationLog WITH(SERIALIZABLE)
SET StartTime = CASE
WHEN StartTime > @StartTime THEN @StartTime
ELSE StartTime
END,
EndTime = CASE
WHEN EndTime < @EndTime THEN @EndTime
ELSE EndTime
END,
Updated = GETUTCDATE()
WHERE Who = @Who
AND (
StartTime BETWEEN @RangeStart and @RangeEnd
or
EndTime BETWEEN @RangeStart and @RangeEnd
)
AND cast(Latitude as decimal(8,5)) = cast(@Latitude as decimal(8, 5))
AND cast(Longitude as decimal(8,5)) = cast(@Longitude as decimal(8, 5))
AND (Accuracy = @Accuracy or COALESCE(Accuracy, @Accuracy) is NULL)
AND (Altitude = @Altitude or COALESCE(Altitude, @Altitude) is NULL)
AND (AltitudeAccuracy = @AltitudeAccuracy or COALESCE(AltitudeAccuracy, @AltitudeAccuracy) is NULL)
AND (Heading = @Heading or COALESCE(Heading, @Heading) is NULL)
AND (Speed = @Speed or COALESCE(Speed, @Speed) is NULL);
IF @@ROWCOUNT = 0
BEGIN
INSERT Position(UUID, Who, StartTime, EndTime, Latitude, Longitude, Accuracy, Altitude, AltitudeAccuracy, Heading, Speed, CreatedTime, Updated)
VALUES (NEWID(), @Who, @StartTime, @EndTime, @Latitude, @Longitude, @Accuracy, @Altitude, @AltitudeAccuracy, @Heading, @Speed, GETUTCDATE(), GETUTCDATE())
END
COMMIT TRAN
我正在使用带有事务和可序列化的标准 "update, if @@rowcount = 0 insert",这(据我所知)与 Sam Saffron's "Insert or Update pattern for Sql Server" Blog post 相同,除了我没有使用单个列 ID,我正在使用大量候选列,因为无法以编程方式生成单个候选键。
并发调用时出现死锁,我不明白为什么。只是为了帮助完成图片,这里是 table 定义:
CREATE TABLE LocationLog (
[UUID] [uniqueidentifier] NOT NULL CONSTRAINT [PK_Position] PRIMARY KEY NONCLUSTERED ,
[Who] [uniqueidentifier] NOT NULL INDEX [IX_Who],
[StartTime] [datetime] NOT NULL,
[EndTime] [datetime] NULL,
[Latitude] [decimal](9, 6) NOT NULL,
[Longitude] [decimal](9, 6) NOT NULL,
[Accuracy] [float] NULL,
[Altitude] [float] NULL,
[AltitudeAccuracy] [float] NULL,
[Heading] [float] NULL,
[Speed] [float] NULL,
[CreatedUtc] [datetime] NOT NULL,
[UpdatedUtc] [datetime] NOT NULL
)
这里有一个脚本,使用上面的 sql 会导致 很多 死锁:https://dotnetfiddle.net/xkze6l
我希望在回答中包含两点:
- 解释为什么会出现死锁。 (我已经遍历了代码,但我不明白我做错了什么)。
- 修复代码,让我可以这样做。
这个谓词:
AND (
StartTime BETWEEN @RangeStart and @RangeEnd
or
EndTime BETWEEN @RangeStart and @RangeEnd
)
AND cast(Latitude as decimal(8,5)) = cast(@Latitude as decimal(8, 5))
AND cast(Longitude as decimal(8,5)) = cast(@Longitude as decimal(8, 5))
将需要 table 扫描。因此,如果您打算每次都扫描 table,您最好使用 TABLOCKX 提示而不是 SERIALIZABLE。
我推荐阅读 Dan Guzman 的 Conditional INSERT/UPDATE Race Condition。
您很可能需要在 UPDATE
语句中添加 HOLDLOCK
提示。需要 HOLDLOCK
来确保锁一直保持到事务结束。
据我了解,您的 WITH(SERIALIZABLE)
提示仅适用于 UPDATE
语句,不会影响下一个 INSERT
语句。
这解释了为什么会出现死锁 - 锁仅在 UPDATE
语句期间持有,然后释放,这允许另一个会话在 INSERT
之前挤入.
也许您还需要 UPDLOCK
,因为即使它是一个 UPDATE
语句,它也必须首先找到要更新的行,并且应该在此期间放置锁 SELECT 阶段,在 UPDATE 阶段之前。
我正在尝试编写一些 SQL 来根据相当复杂的条件进行更新:
BEGIN TRAN;
UPDATE LocationLog WITH(SERIALIZABLE)
SET StartTime = CASE
WHEN StartTime > @StartTime THEN @StartTime
ELSE StartTime
END,
EndTime = CASE
WHEN EndTime < @EndTime THEN @EndTime
ELSE EndTime
END,
Updated = GETUTCDATE()
WHERE Who = @Who
AND (
StartTime BETWEEN @RangeStart and @RangeEnd
or
EndTime BETWEEN @RangeStart and @RangeEnd
)
AND cast(Latitude as decimal(8,5)) = cast(@Latitude as decimal(8, 5))
AND cast(Longitude as decimal(8,5)) = cast(@Longitude as decimal(8, 5))
AND (Accuracy = @Accuracy or COALESCE(Accuracy, @Accuracy) is NULL)
AND (Altitude = @Altitude or COALESCE(Altitude, @Altitude) is NULL)
AND (AltitudeAccuracy = @AltitudeAccuracy or COALESCE(AltitudeAccuracy, @AltitudeAccuracy) is NULL)
AND (Heading = @Heading or COALESCE(Heading, @Heading) is NULL)
AND (Speed = @Speed or COALESCE(Speed, @Speed) is NULL);
IF @@ROWCOUNT = 0
BEGIN
INSERT Position(UUID, Who, StartTime, EndTime, Latitude, Longitude, Accuracy, Altitude, AltitudeAccuracy, Heading, Speed, CreatedTime, Updated)
VALUES (NEWID(), @Who, @StartTime, @EndTime, @Latitude, @Longitude, @Accuracy, @Altitude, @AltitudeAccuracy, @Heading, @Speed, GETUTCDATE(), GETUTCDATE())
END
COMMIT TRAN
我正在使用带有事务和可序列化的标准 "update, if @@rowcount = 0 insert",这(据我所知)与 Sam Saffron's "Insert or Update pattern for Sql Server" Blog post 相同,除了我没有使用单个列 ID,我正在使用大量候选列,因为无法以编程方式生成单个候选键。
并发调用时出现死锁,我不明白为什么。只是为了帮助完成图片,这里是 table 定义:
CREATE TABLE LocationLog (
[UUID] [uniqueidentifier] NOT NULL CONSTRAINT [PK_Position] PRIMARY KEY NONCLUSTERED ,
[Who] [uniqueidentifier] NOT NULL INDEX [IX_Who],
[StartTime] [datetime] NOT NULL,
[EndTime] [datetime] NULL,
[Latitude] [decimal](9, 6) NOT NULL,
[Longitude] [decimal](9, 6) NOT NULL,
[Accuracy] [float] NULL,
[Altitude] [float] NULL,
[AltitudeAccuracy] [float] NULL,
[Heading] [float] NULL,
[Speed] [float] NULL,
[CreatedUtc] [datetime] NOT NULL,
[UpdatedUtc] [datetime] NOT NULL
)
这里有一个脚本,使用上面的 sql 会导致 很多 死锁:https://dotnetfiddle.net/xkze6l
我希望在回答中包含两点:
- 解释为什么会出现死锁。 (我已经遍历了代码,但我不明白我做错了什么)。
- 修复代码,让我可以这样做。
这个谓词:
AND (
StartTime BETWEEN @RangeStart and @RangeEnd
or
EndTime BETWEEN @RangeStart and @RangeEnd
)
AND cast(Latitude as decimal(8,5)) = cast(@Latitude as decimal(8, 5))
AND cast(Longitude as decimal(8,5)) = cast(@Longitude as decimal(8, 5))
将需要 table 扫描。因此,如果您打算每次都扫描 table,您最好使用 TABLOCKX 提示而不是 SERIALIZABLE。
我推荐阅读 Dan Guzman 的 Conditional INSERT/UPDATE Race Condition。
您很可能需要在 UPDATE
语句中添加 HOLDLOCK
提示。需要 HOLDLOCK
来确保锁一直保持到事务结束。
据我了解,您的 WITH(SERIALIZABLE)
提示仅适用于 UPDATE
语句,不会影响下一个 INSERT
语句。
这解释了为什么会出现死锁 - 锁仅在 UPDATE
语句期间持有,然后释放,这允许另一个会话在 INSERT
之前挤入.
也许您还需要 UPDLOCK
,因为即使它是一个 UPDATE
语句,它也必须首先找到要更新的行,并且应该在此期间放置锁 SELECT 阶段,在 UPDATE 阶段之前。