触发导致死锁?

Jos*_* M. 8 sql-server triggers deadlock sql-server-2008 database-deadlocks

我添加了一个触发器后,我遇到了僵局.有一个UserBalanceHistory表,每个事务和一Amount列有一行.添加了一个触发器来对Amount列进行求和,并将结果放在相关的UserBalance列中.

CREATE TABLE [User]
(
    ID INT IDENTITY,
    Balance MONEY,
    CONSTRAINT PK_User PRIMARY KEY (ID)
);

CREATE TABLE UserBalanceHistory
(
    ID INT IDENTITY,
    UserID INT NOT NULL,
    Amount MONEY NOT NULL,
    CONSTRAINT PK_UserBalanceHistory PRIMARY KEY (ID),
    CONSTRAINT FK_UserBalanceHistory_User FOREIGN KEY (UserID) REFERENCES [User] (ID)
);

CREATE NONCLUSTERED INDEX IX_UserBalanceHistory_1 ON UserBalanceHistory (UserID) INCLUDE (Amount);

CREATE TRIGGER TR_UserBalanceHistory_1 ON UserBalanceHistory AFTER INSERT, UPDATE, DELETE AS
BEGIN
    DECLARE @UserID INT;

    SELECT TOP 1 @UserID = u.UserID
    FROM
    (
            SELECT UserID FROM inserted
        UNION
            SELECT UserID FROM deleted
    ) u;

    EXEC dbo.UpdateUserBalance @UserID;
END;

CREATE PROCEDURE UpdateUserBalance
    @UserID INT
AS
BEGIN
    DECLARE @Balance MONEY;

    SET @Balance = (SELECT SUM(Amount) FROM UserBalanceHistory WHERE UserID = @UserID);

    UPDATE [User]
    SET Balance = ISNULL(@Balance, 0)
    WHERE ID = @UserID;
END;
Run Code Online (Sandbox Code Playgroud)

我也打开了READ_COMMITTED_SNAPSHOT:

ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON;
Run Code Online (Sandbox Code Playgroud)

我有一个正在运行的并行进程正在创建UserBalanceHistory条目,显然如果它同时工作User,则会发生死锁.建议?

Dar*_*ark 5

这是一个老问题了,但我想如果其他人遇到它,我就找到了答案。当然是我的答案。

问题可能是 UserBalanceHistory 和 User 之间存在 FK 约束。在这种情况下,对 UserBalanceHistory 的两个并发插入可能会发生死锁。

这是因为在插入 UserBalanceHistory 时,数据库将在 User 上获取共享锁以查找 FK 的 ID。然后,当触发器触发时,它将对用户进行独占锁定。

如果这种情况同时发生,则这是典型的锁升级死锁,其中两个事务都无法升级为独占锁,因为另一个事务持有共享锁。

我的解决方案是在更新和插入时无偿地加入到 User 表,并在该表上使用WITH (UPDLOCK) 提示。


gbn*_*gbn 3

发生死锁是因为您正在访问 UserBalanceHistory -> UserBalanceHistory -> User 而其他一些更新是 User -> UserBalanceHistory。由于锁粒度和索引锁等原因,它比这更复杂。

根本原因可能是在 UserBalanceHistory 上扫描 UserID 和 Amount。我会(UserID) INCLUDE (Amount)在 UserBalanceHistory 上建立一个索引来更改此设置

SNAPSHOT 隔离模型仍然可能出现死锁:有一些示例(

最后,为什么不将这一切都集中在一起以避免不同的和多个更新路径呢?

CREATE TRIGGER TR_UserBalanceHistory_1 ON UserBalanceHistory AFTER INSERT, UPDATE, DELETE AS
BEGIN
    DECLARE @UserID INT;

    UPDATE U
    SET Balance = ISNULL(t2.Balance, 0)
    FROM
       (
         SELECT UserID FROM INSERTED
         UNION
         SELECT UserID FROM DELETED
       ) t1
       JOIN
       [User] U ON t1.UserID = u.UserID
       LEFT JOIN
       (
        SELECT UserID, SUM(Amount) AS Balance
        FROM UserBalanceHistory
        GROUP BY UserID
       ) t2 ON t1.UserID = t2.UserID;

END;
Run Code Online (Sandbox Code Playgroud)