Синхронизация с использованием триггеров

11

У меня есть требование, аналогичное предыдущим обсуждениям по адресу:

У меня есть две таблицы, [Account].[Balance]и [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

CREATE TABLE Transaction (
      TransactionID INT
     , AccountID    INT
    , Amount      MONEY
);

Когда есть вставка, обновление или удаление в [Transaction]таблице, они [Account].[Balance]должны обновляться на основе [Amount].

В настоящее время у меня есть триггер, чтобы сделать эту работу:

ALTER TRIGGER [dbo].[TransactionChanged] 
ON  [dbo].[Transaction]
AFTER INSERT, UPDATE, DELETE
AS 
BEGIN
IF  EXISTS (select 1 from [Deleted]) OR EXISTS (select 1 from [Inserted])
    UPDATE [dbo].[Account]
    SET
    [Account].[Balance] = [Account].[Balance] + 
        (
            Select ISNULL(Sum([Inserted].[Amount]),0)
            From [Inserted] 
            Where [Account].[AccountID] = [Inserted].[AccountID]
        )
        -
        (
            Select ISNULL(Sum([Deleted].[Amount]),0)
            From [Deleted] 
            Where [Account].[AccountID] = [Deleted].[AccountID]
        )
END

Хотя это, кажется, работает, у меня есть вопросы:

  1. Соответствует ли триггер принципу реляционной базы данных ACID? Есть ли вероятность того, что вставка может быть зафиксирована, но триггер не работает?
  2. Мои IFи UPDATEзаявления выглядят странно. Есть ли лучший способ обновить правильную [Account]строку?
Yiping
источник

Ответы:

13

1. Соответствует ли триггер принципу реляционной базы данных ACID? Есть ли вероятность того, что вставка может быть зафиксирована, но триггер не работает?

На этот вопрос частично дан ответ в связанном с вами вопросе . Триггерный код выполняется в том же транзакционном контексте, что и оператор DML, вызвавший его срабатывание, сохраняя атомарную часть принципов ACID, о которых вы упомянули. Оператор триггера и код триггера успешно выполняются или не выполняются как единое целое.

Эти свойства ACID также гарантировать всю транзакцию (включая код запуска) оставят базу данных в состоянии , которое не нарушает какие - либо явные ограничения ( Последовательные ) и любые извлекаемые совершенные эффекты сохраняются в случае сбой базы данных ( Durable ).

Если окружающая (возможно, неявная или автоматическая фиксация) транзакция не выполняется на SERIALIZABLEуровне изоляции , свойство Isolated не гарантируется автоматически. Другая одновременная активность базы данных может помешать правильной работе вашего кода триггера. Например, баланс аккаунта может быть изменен другим сеансом после его прочтения и перед обновлением - классическое условие гонки.

2. Мои заявления IF и UPDATE выглядят странно. Есть ли лучший способ обновить правильную строку [Account]?

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

Поддержание хорошей производительности в то же время, как сохранение правильности во всех сценариях и избежание проблем, таких как взаимоблокировки, добавляет дополнительные аспекты сложности. Ваш код триггера далеко не является надежным и обновляет баланс каждой учетной записи, даже если изменена только одна транзакция. Решение на основе триггера сопряжено с множеством рисков и проблем, что делает задачу крайне непригодной для тех, кто относительно новичок в этой области технологий.

Чтобы проиллюстрировать некоторые проблемы, ниже приведен пример кода. Это не строго проверенное решение (триггеры трудны!), И я не предлагаю вам использовать его в качестве чего-либо, кроме учебного упражнения. Для реальной системы решения без триггера имеют важные преимущества, поэтому вам следует внимательно изучить ответы на другой вопрос и полностью избежать идеи триггера.

Образцы таблиц

CREATE TABLE dbo.Accounts
(
    AccountID integer NOT NULL,
    Balance money NOT NULL,

    CONSTRAINT PK_Accounts_ID
    PRIMARY KEY CLUSTERED (AccountID)
);

CREATE TABLE dbo.Transactions
(
    TransactionID integer IDENTITY NOT NULL,
    AccountID integer NOT NULL,
    Amount money NOT NULL,

    CONSTRAINT PK_Transactions_ID
    PRIMARY KEY CLUSTERED (TransactionID),

    CONSTRAINT FK_Accounts
    FOREIGN KEY (AccountID)
    REFERENCES dbo.Accounts (AccountID)
);

предотвращение TRUNCATE TABLE

Триггеры не запускаются TRUNCATE TABLE. Следующая пустая таблица существует исключительно для предотвращения Transactionsусечения таблицы (ссылка на внешний ключ предотвращает усечение таблицы):

CREATE TABLE dbo.PreventTransactionsTruncation
(
    Dummy integer NULL,

    CONSTRAINT FK_Transactions
    FOREIGN KEY (Dummy)
    REFERENCES dbo.Transactions (TransactionID),

    CONSTRAINT CHK_NoRows
    CHECK (Dummy IS NULL AND Dummy IS NOT NULL)
);

Определение триггера

Следующий триггерный код обеспечивает ведение только необходимых записей учетной записи и использует SERIALIZABLEтам семантику. Как желательный побочный эффект, это также позволяет избежать неверных результатов, которые могут возникнуть, если используется уровень изоляции управления версиями строк. Код также избегает выполнения кода триггера, если ни одна строка не была затронута исходной инструкцией. Временная таблица и RECOMPILEподсказка используются, чтобы избежать проблем плана выполнения триггера, вызванных неточными оценками количества элементов:

CREATE TRIGGER dbo.TransactionChange ON dbo.Transactions 
AFTER INSERT, UPDATE, DELETE 
AS
BEGIN
IF @@ROWCOUNT = 0 OR
    TRIGGER_NESTLEVEL
    (
        OBJECT_ID(N'dbo.TransactionChange', N'TR'),
        'AFTER', 
        'DML'
    ) > 1 
    RETURN;

    SET NOCOUNT, XACT_ABORT ON;

    CREATE TABLE #Delta
    (
        AccountID integer PRIMARY KEY,
        Amount money NOT NULL
    );

    INSERT #Delta
        (AccountID, Amount)
    SELECT 
        InsDel.AccountID,
        Amount = SUM(InsDel.Amount)
    FROM 
    (
        SELECT AccountID, Amount
        FROM Inserted
        UNION ALL
        SELECT AccountID, $0 - Amount
        FROM Deleted
    ) AS InsDel
    GROUP BY
        InsDel.AccountID;

    UPDATE A
    SET Balance += D.Amount
    FROM #Delta AS D
    JOIN dbo.Accounts AS A WITH (SERIALIZABLE)
        ON A.AccountID = D.AccountID
    OPTION (RECOMPILE);
END;

тестирование

Следующий код использует таблицу чисел для создания 100 000 учетных записей с нулевым балансом:

INSERT dbo.Accounts
    (AccountID, Balance)
SELECT
    N.n, $0
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 100000;

Тестовый код ниже вставляет 10000 случайных транзакций:

INSERT dbo.Transactions
    (AccountID, Amount)
SELECT 
    CONVERT(integer, RAND(CHECKSUM(NEWID())) * 100000 + 1),
    CONVERT(money, RAND(CHECKSUM(NEWID())) * 500 - 250)
FROM dbo.Numbers AS N
WHERE 
    N.n BETWEEN 1 AND 10000;

Используя инструмент SQLQueryStress , я провел этот тест 100 раз на 32 потоках с хорошей производительностью, без блокировок и с правильными результатами. Я до сих пор не рекомендую это как что-либо кроме учебного упражнения.

Пол Уайт 9
источник