Триггер, вызывающий тупик? - PullRequest
8 голосов
/ 08 июня 2011

Я зашел в тупик после того, как добавил триггер. Существует таблица 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 ]

2 голосов
/ 08 июня 2011

Взаимная блокировка возникает из-за того, что вы обращаетесь к UserBalanceHistory -> UserBalanceHistory -> User, тогда как другим обновлением является User -> UserBalanceHistory.Это сложнее, чем из-за гранулярности блокировок, блокировок индексов и т. Д.

Основная причина, вероятно, заключается в сканировании UserBalanceHistory на наличие идентификатора пользователя и количества.Для изменения этой модели изоляции

SNAPSHOT все еще может возникать взаимная блокировка для индекса (UserID) INCLUDE (Amount) в UserBalanceHistory: есть примеры ( Один , Два

Наконец, почему бы не сделать все это в одном, чтобы избежать разных и множественных путей обновления?

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;
0 голосов
/ 13 февраля 2019

Старый вопрос, но я думаю, что только что нашел ответ, если кто-нибудь еще сталкивался с этим.Конечно, был ответ для меня.

Проблема, вероятно, в том, что существует ограничение FK между UserBalanceHistory и User.В этом случае две одновременные вставки в UserBalanceHistory могут привести к взаимоблокировке.

Это связано с тем, что при вставке в UserBalanceHistory база данных будет использовать общую блокировку для пользователя для поиска идентификатора FK.Затем, когда сработает триггер, он получит эксклюзивную блокировку для пользователя.

Если это происходит одновременно, это классический тупик эскалации блокировки, когда ни одна транзакция не может перерасти в монопольную блокировку, потому что другая удерживает общую блокировку.

Мое решение состояло в том, чтобы бесплатно присоединиться кПользовательская таблица обновлений, вставок и использование подсказки WITH (UPDLOCK) для этой таблицы.

0 голосов
/ 09 июня 2011

Измените кластеризованный ключ на userid в вашей таблице UserBalanceHistory и удалите некластеризованный индекс, потому что вы используете userid для доступа к таблице, нет причин использовать столбец идентификаторов для кластеризованного индекса, так как это всегда вызываеткластерный индекс, который будет использоваться, а затем чтение из кластерного индекса для изменения денежной стоимости.Кластерные индексы лучше всего подходят для поиска по диапазону. Это то, что вы делаете при суммировании баланса.Ваша текущая ситуация может привести к тому, что SQL будет запрашивать каждую страницу данных в таблице только для получения пользовательских платежей, некоторая фрагментация в кластерном индексе компенсируется случайно (sp) связанными страницами для одного идентификатора пользователя.Изменение кластера и удаление некластера сэкономит время и память.
Не запускайте ни один сохраненный процесс из триггера, поскольку он заблокирует триггерную таблицу, когда SP завершит работу.

Таблица баланса может бытьсделан из представления с вычисляемым столбцом (ссылка SO здесь ) в таблице UserBalanceHistory.

Тестирование в системе разработки, а затем тестирование снова!

...