解决仅通过索引视图相关的2个表的死锁


17

我遇到了陷入僵局的情况,我想我已经缩小了罪魁祸首的位置,但是我不确定该如何解决。

这是在运行SQL Server 2008 R2的生产环境中。

为了让您对情况略有简化:


我有3个表,定义如下:

TABLE activity (
    id, -- PK
    ...
)

TABLE member_activity (
    member_id, -- PK col 1
    activity_id, -- PK col 2
    ...
)

TABLE follow (
    id, -- PK
    follower_id,
    member_id,
    ...
)

member_activity表具有定义为的复合主键member_id, activity_id,因为我只需要以这种方式在该表上查找数据。

我也有一个非聚集索引follow

CREATE NONCLUSTERED INDEX [IX_follow_member_id_includes] 
ON follow ( member_id ASC ) INCLUDE ( follower_id )

另外,我有一个架构绑定视图network_activity,其定义如下:

CREATE VIEW network_activity
WITH SCHEMABINDING
AS

SELECT
    follow.follower_id as member_id,
    member_activity.activity_id as activity_id,
    COUNT_BIG(*) AS cb
FROM member_activity
INNER JOIN follow ON follow.member_id = member_activity.member_id
INNER JOIN activity ON activity.id = member_activity.activity_id
GROUP BY follow.follower_id, member_activity.activity_id

其中也具有唯一的聚集索引:

CREATE UNIQUE CLUSTERED INDEX [IX_network_activity_unique_member_id_activity_id] 
ON network_activity
(
    member_id ASC,
    activity_id ASC
)

现在,我有两个死锁的存储过程。他们经历以下过程:

-- SP1: insert activity
-----------------------
INSERT INTO activity (...)
SELECT ... FROM member_activity WHERE member_id = @a AND activity_id = @b
INSERT INTO member_activity (...)


-- SP2: insert follow
---------------------
SELECT follow WHERE member_id = @x AND follower_id = @y
INSERT INTO follow (...)

这两个过程都以READ COMMITTED隔离运行。我设法查询了1222个扩展事件的输出,并就死锁进行了以下解释:

SP1正在等待索引RangeS-S上的键锁,IX_follow_member_id_includes而SP2则持有冲突(X)锁

SP2正在等待S模式锁定,PK_member_activity 而SP1持有冲突(X)锁定

死锁似乎发生在每个查询(插入)的最后一行。我不清楚的是为什么SP1想要锁定IX_follow-member_id_includes索引。对我来说,唯一的链接似乎是从此索引视图开始的,这就是为什么我将其包括在内。

对我来说,防止这些僵局发生的最佳方法是什么?任何帮助将非常感激。我在解决死锁问题上经验不足。

请让我知道是否还有其他可以帮助您的信息!

提前致谢。


编辑1:为每个请求添加更多信息。

这是此死锁的1222输出:

<deadlock>
    <victim-list>
        <victimProcess id="process4c6672748" />
    </victim-list>
    <process-list>
        <process id="process4c6672748" taskpriority="0" logused="332" waitresource="KEY: 8:72057594104905728 (25014f77eaba)" waittime="581" ownerId="474698706" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.287" XDES="0x298487970" lockMode="RangeS-S" schedulerid="1" kpid="972" status="suspended" spid="79" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T10:25:00.283" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698706" currentdb="8" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
            <executionStack>
                <frame procname="" line="7" stmtstart="1194" stmtend="1434" sqlhandle="0x02000000a26bb72a2b220406876cad09c22242e5265c82e6" />
                <frame procname="" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 1 --> </inputbuf>
        </process>
        <process id="process6cddc5b88" taskpriority="0" logused="456" waitresource="KEY: 8:72057594098679808 (89013169fc76)" waittime="567" ownerId="474698698" transactionname="INSERT" lasttranstarted="2014-07-03T17:03:12.283" XDES="0x30c459970" lockMode="S" schedulerid="4" kpid="4204" status="suspended" spid="70" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2014-07-03T17:03:12.283" lastbatchcompleted="2014-07-03T17:03:12.283" lastattention="2014-07-03T15:04:55.870" clientapp=".Net SqlClient Data Provider" hostname="WIN08CLYDESDALE" hostpid="4596" loginname="TechPro" isolationlevel="read committed (2)" xactid="474698698" currentdb="8" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
            <executionStack>
                <frame procname="" line="18" stmtstart="942" stmtend="1250" sqlhandle="0x03000800ca458d315ee9130100a300000100000000000000" />
            </executionStack>
            <inputbuf> <!-- SP 2 --> </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <keylock hobtid="72057594104905728" dbid="8" objectname="" indexname="" id="lock33299fc00" mode="X" associatedObjectId="72057594104905728">
            <owner-list>
                <owner id="process6cddc5b88" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process4c6672748" mode="RangeS-S" requestType="wait" />
            </waiter-list>
        </keylock>
        <keylock hobtid="72057594098679808" dbid="8" objectname="" indexname="" id="lockb7e2ba80" mode="X" associatedObjectId="72057594098679808">
            <owner-list>
                <owner id="process4c6672748" mode="X" />
            </owner-list>
            <waiter-list>
                <waiter id="process6cddc5b88" mode="S" requestType="wait" />
            </waiter-list>
        </keylock>
    </resource-list>
</deadlock>

在这种情况下,

relatedObjectId 72057594098679808对应于 member_activity, PK_member_activity

relatedObjectId 72057594104905728对应于 follow, IX_follow_member_id_includes

此外,这是SP1和SP2正在做什么的更精确的图片

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m1 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m1, @activityId, @field1)

IF NOT EXISTS(
    SELECT TOP 1 member_id 
    FROM member_activity 
    WHERE member_id = @m2 AND activity_id = @activityId
)
    INSERT INTO member_activity (member_id, activity_id, field1)
    VALUES (@m2, @activityId, @field1)

同样是SP2:

-- SP2: insert follow
---------------------

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

编辑2:重新阅读评论后,我想我还要添加有关哪些列是外键的信息...

  • member_activity.member_idmember表的外键
  • member_activity.activity_idactivity表的外键
  • follow.member_idmember表的外键
  • follow.follower_idmember表的外键

更新1:

我进行了一些更改,我认为这些更改可能有助于防止死锁,但是没有运气。

我所做的更改如下:

-- SP1: insert activity
-----------------------
DECLARE @activityId INT

INSERT INTO activity (field1, field2)
VALUES (@field1, @field2)

SET @activityId = SCOPE_IDENTITY();

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m1 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

MERGE member_activity WITH ( HOLDLOCK ) as target
USING (SELECT @m2 as member_id, @activityId as activity_id, @field1 as field1) as source
    ON target.member_id = source.member_id
    AND target.activity_id = source.activity_id
WHEN NOT MATCHED THEN
    INSERT (member_id, activity_id, field1)
    VALUES (source.member_id, source.activity_id, source.field1)
;

并使用SP2:

-- SP2: insert follow
---------------------

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION

IF NOT EXISTS(
    SELECT TOP 1 1 
    FROM follow WITH ( UPDLOCK )
    WHERE member_id = @memberId AND follower_id = @followerId
)
    INSERT INTO follow (member_id, follower_id)
    VALUES (@memberId, @followerId)

COMMIT

通过这两个更改,我似乎仍然陷入僵局。

如果还有其他可以提供的信息,请告诉我。谢谢。


提交的读操作不使用键范围锁,而仅可序列化。如果实际上死锁确实显示已读已提交(2),那么我的猜测是您正在访问更改外键,该外键将在后台转换为可序列化的(尽管仍然说已读已提交)。老实说,我们需要整个ddl和sp来提供进一步的帮助。
肖恩说删除Sara Chipps

@SeanGallardy,谢谢。我进行了编辑以包括1222输出,以防万一我解释错误,并且还添加了有关SP正在执行的操作的更多详细信息。这有帮助吗?
Leland Richardson

2
@SeanGallardy维护索引视图的查询计划部分在内部运行SERIALIZABLE(不止于此,但这不是评论,而是评论:)
Paul White恢复莫妮卡

@PaulWhite谢谢您的见识,我不知道!做一个快速测试,我肯定可以在插入存储过程(RangeI-N,RangeS-S,RangeS-U)的过程中获得带有索引视图的可序列化锁定模式。似乎死锁是由于在存储过程中的插入在锁边界内(例如在范围锁所保持的区域内)时,在不正确的锁模式下在正确的时间相互碰撞而发生的。我会认为定时和输入数据冲突。
肖恩说删除Sara Chipps

问题:如果我在SELECT语句上添加了HOLDLOCK提示,是否可以防止在插入时发生锁定?
Leland Richardson

Answers:


5

冲突归结为network_activity需要在DML语句中(内部)维护的索引视图。这很可能是SP1想要锁定IX_follow-member_id_includes索引的原因,因为它可能被View使用(它似乎是View的覆盖索引)。

两种可能的选择:

  1. 考虑将聚簇索引放在视图上,以使其不再是索引视图。这样做的好处是否超过维护成本?您是否经常从中选择它,或者索引它所带来的性能提升值得吗?如果您经常运行这些proc,那么成本可能会高于收益?

  2. 如果为视图建立索引的好处确实超过了成本,请考虑将DML操作与该视图的基表隔离。这可以通过使用应用程序锁来完成(请参见sp_getapplock sp_releaseapplock)。应用程序锁使您可以围绕任意概念创建锁。意思是,您可以@Resource在两个存储过程中都将“ network_activity” 定义为“ network_activity”,这将迫使他们等待轮到他们。每个过程将遵循相同的结构:

    BEGIN TRANSACTION;
    EXEC sp_getapplock @Resource = 'network_activity', @LockMode = 'Exclusive';
    ...current proc code...
    EXEC sp_releaseapplock @Resource = 'network_activity';
    COMMIT TRANSACTION;

    您需要自己管理错误ROLLBACK(如链接的MSDN文档中所述),因此请按常规进行操作TRY...CATCH。但是,这确实可以管理情况。
    请注意: sp_getapplock / sp_releaseapplock应该谨慎使用;应用程序锁绝对可以非常方便(例如在这种情况下),但是只有在绝对必要时才应使用它们。


谢谢您的帮助。我将对选项2进行更多阅读,看看是否对我们有用。从相当多的角度读取了视图,并且聚集索引是一个很大的帮助...所以我宁愿不删除它。一试,我会回来更新。
Leland Richardson

我认为使用sp_getapplock将起作用。我还不能在我们的生产环境中尝试它,但是我想确保您在赏金过期之前获得了赏金。可以确认的话,我会在这里更新!
利兰·理查森

谢谢。关于“应用程序锁”的一件好事是,您可以更改将诸如之类的东西串联member_id@Resource值中的粒度级别。这似乎不适用于这种特殊情况,但我已经看到它的使用方式非常方便,尤其是在多租户系统中,您希望根据每个客户将流程限制为一个线程,但是仍然有跨客户的多线程。
所罗门·鲁兹基

我想提供一个更新,并说这确实可以在我们的生产环境中工作。:)
Leland Richardson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.