【问题标题】:Trigger causing a deadlock?触发导致死锁?
【发布时间】:2011-06-08 17:04:36
【问题描述】:

添加触发器后,我遇到了死锁。有一个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;

我也开启了READ_COMMITTED_SNAPSHOT

ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON;

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

【问题讨论】:

  • 我想让你明白你的触发代码非常糟糕和危险。您永远不能假设插入或删除只有一条记录。当有人第一次需要运行基于集合的插入(例如从新客户端导入历史数据)时,此代码会导致严重的数据完整性问题
  • 是的,很好 - 我会解决这个问题。

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


【解决方案1】:

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

根本原因可能是在 UserBalanceHistory 上扫描 UserID 和 Amount。我会在 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;

【讨论】:

  • 我已经实现了这个,但我仍然遇到了死锁。
  • @Josh M.:同样的死锁图?带索引?带触发器?
  • 是的。我实际上已经有了索引,但忽略了将它包含在我的问题中。
【解决方案2】:

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

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

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

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

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

【讨论】:

    【解决方案3】:

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

    余额表可以由 UserBalanceHistory 表上具有计算列(SO 链接 here)的视图制成。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-20
      • 2012-09-20
      • 2018-06-04
      • 2016-12-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多