Synchronizacja za pomocą wyzwalaczy

11

Mam wymaganie podobne do poprzednich dyskusji na:

Mam dwie tabele [Account].[Balance]i [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

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

W przypadku wstawienia, aktualizacji lub usunięcia w stosunku do [Transaction]tabeli [Account].[Balance]należy ją zaktualizować na podstawie [Amount].

Obecnie mam wyzwalacz do wykonania tej pracy:

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

Chociaż wydaje się, że to działa, mam pytania:

  1. Czy wyzwalacz jest zgodny z zasadą ACID relacyjnej bazy danych? Czy istnieje szansa, że ​​wstawka może zostać zatwierdzona, ale wyzwalacz nie powiedzie się?
  2. Moje IFi UPDATEwypowiedzi wyglądają dziwnie. Czy istnieje lepszy sposób zaktualizowania prawidłowego [Account]wiersza?
Yiping
źródło

Odpowiedzi:

13

1. Czy wyzwalacz jest zgodny z zasadą ACID relacyjnej bazy danych? Czy istnieje szansa, że ​​wstawka może zostać zatwierdzona, ale wyzwalacz nie powiedzie się?

Odpowiedź na to pytanie jest częściowo udzielona w powiązanym z nim pytaniu . Kod wyzwalający jest wykonywany w tym samym kontekście transakcyjnym, co instrukcja DML, która spowodowała jego uruchomienie, zachowując część atomową zasad ACID, o których wspomniałeś. Instrukcja wyzwalająca i kod wyzwalający zarówno działają jak i kończą się niepowodzeniem jako jednostka.

Właściwości ACID gwarantują również, że cała transakcja (łącznie z kodem wyzwalacza) opuści bazę danych w stanie, który nie narusza żadnych wyraźnych ograniczeń ( spójne ), a wszelkie przywracane zatwierdzone efekty przetrwają awarię bazy danych ( trwałą ).

O ile otaczająca transakcja (być może niejawna lub autoryzacja) nie działa na SERIALIZABLEpoziomie izolacji , właściwość Izolacja nie jest automatycznie gwarantowana. Inna jednoczesna aktywność bazy danych może zakłócać prawidłowe działanie kodu wyzwalacza. Na przykład saldo konta może zostać zmienione przez inną sesję po jego przeczytaniu i przed aktualizacją - klasyczny warunek wyścigu.

2. Moje instrukcje IF i UPDATE wyglądają dziwnie. Czy istnieje lepszy sposób zaktualizowania prawidłowego wiersza [Konto]?

Istnieją bardzo dobre powody, dla których drugie pytanie, do którego się odnosisz, nie oferuje żadnych rozwiązań opartych na wyzwalaczach. Kod wyzwalający zaprojektowany do synchronizacji zdenormalizowanej struktury może być bardzo trudny do poprawnego wykonania i prawidłowego przetestowania. Walczą z tym nawet bardzo zaawansowani ludzie programu SQL Server z wieloletnim doświadczeniem.

Utrzymanie dobrej wydajności przy jednoczesnym zachowaniu poprawności we wszystkich scenariuszach i unikanie problemów takich jak zakleszczenia dodaje dodatkowych wymiarów trudności. Twój kod aktywacyjny nie jest zbyt blisko, aby być niezawodnym i aktualizuje saldo każdego konta, nawet jeśli zmodyfikowana jest tylko jedna transakcja. Istnieje wiele rodzajów ryzyka i wyzwań związanych z rozwiązaniem opartym na wyzwalaczu, co sprawia, że ​​zadanie jest całkowicie nieodpowiednie dla kogoś stosunkowo nowego w tej dziedzinie technologii.

Aby zilustrować niektóre problemy, poniżej pokazuję przykładowy kod. To nie jest dokładnie przetestowane rozwiązanie (wyzwalacze są trudne!) I nie sugeruję, abyś używał go jako czegoś innego niż ćwiczenie edukacyjne. W prawdziwym systemie rozwiązania nie wyzwalające mają ważne zalety, dlatego należy dokładnie przejrzeć odpowiedzi na inne pytanie i całkowicie unikać pomysłu na wyzwalacz.

Przykładowe tabele

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)
);

Zapobieganie TRUNCATE TABLE

Wyzwalacze nie są uruchamiane przez TRUNCATE TABLE. Poniższa pusta tabela istnieje wyłącznie po to, aby zapobiec Transactionsobcinaniu tabeli (odwoływanie się do niej przez klucz obcy zapobiega obcinaniu tabeli):

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)
);

Definicja wyzwalacza

Poniższy kod wyzwalacza zapewnia utrzymanie tylko niezbędnych wpisów konta i używa SERIALIZABLEtam semantyki. Jako pożądany efekt uboczny pozwala to również uniknąć niepoprawnych wyników, które mogą wystąpić, jeśli używany jest poziom izolacji wersji wierszowej. Kod unika także wykonywania kodu wyzwalającego, jeśli instrukcja source nie ma wpływu na wiersze. Tabela tymczasowa i RECOMPILEpodpowiedź służą do uniknięcia problemów z planem wykonania wyzwalacza spowodowanych niedokładnymi szacunkami liczności:

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;

Testowanie

Poniższy kod wykorzystuje tabelę liczb do utworzenia 100 000 kont z zerowym saldem:

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

Poniższy kod testowy wstawia 10 000 losowych transakcji:

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;

Za pomocą narzędzia SQLQueryStress uruchomiłem ten test 100 razy w 32 wątkach z dobrą wydajnością, bez zakleszczeń i poprawnymi wynikami. Nadal nie polecam tego jako czegoś innego niż ćwiczenie edukacyjne.

Paul White 9
źródło