触发导致死锁?

时间:2011-06-08 17:04:36

标签: sql-server sql-server-2008 triggers deadlock database-deadlocks

添加触发器后,我遇到了僵局。有一个UserBalanceHistory表,每个事务都有一行,Amount列。添加了一个触发器,以对Amount列求和,并将结果放在相关的User表格Balance列中。

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;

我也打开了READ_COMMITTED_SNAPSHOT

ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON;

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

3 个答案:

答案 0 :(得分:2)

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

根本原因可能是对UserID和Amount的UserBalanceHistory进行扫描。我在UserBalanceHistory上有(UserID) INCLUDE (Amount)的索引来改变这个

SNAPSHOT隔离模型仍然可以死锁:有例子(OneTwo

最后,为什么不一起做一个以避免不同的和多个更新路径?

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;

答案 1 :(得分:0)

将群集密钥更改为UserBalanceHistory表中的userid并删除非聚集索引,因为您使用userid访问该表,没有理由使用聚簇索引的标识列,因为它将始终强制非聚簇索引要使用聚簇索引,然后从聚簇索引读取更改货币值。群集索引最适合范围搜索,这是您在总和余额时所执行的操作。您当前的情况可能会导致SQL请求表中的每个数据页只是为了获得用户付款,聚集索引中的某些碎片会被单个用户ID的可靠(sp)链接页面抵消。更改群集并删除非群集将节省时间和内存 不要从触发器运行任何存储过程,因为它会在SP完成时锁定触发的表。

可以使用UserBalanceHistory表上带有计算列(SO链接here)的视图制作余额表。

在开发系统中测试,然后再次测试!

答案 2 :(得分:0)

一个古老的问题,但是我想我只要找到其他人就可以找到答案。肯定是我的答案。

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

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

如果这是同时发生的,则这是经典的锁升级死锁,其中两个事务都不能升级为互斥锁,因为另一个事务持有共享锁。

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