使用触发器进行同步


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. 我的IFUPDATE陈述看起来很奇怪。有没有更好的方法来更新正确的[Account]行?

Answers:


13

1.触发器是否遵循关系数据库的ACID原则?是否有可能提交插入但触发失败的机会?

您链接到的一个相关问题中,部分回答了该问题。触发代码在与导致触发的DML语句相同的事务上下文中执行,保留了您提到的ACID原理的Atomic部分。触发语句和触发代码都作为一个单元成功或失败。

ACID特性也保证了整个交易(包括触发代码)将离开数据库在不违反任何明确的限制(的状态一致)和可恢复的承诺的影响将生存在数据库崩溃(耐用)。

除非周围的(也许是隐式或自动提交)事务是在运行SERIALIZABLE隔离级别,该隔离属性不会自动保证。其他并发数据库活动可能会干扰触发代码的正确操作。例如,在您阅读帐户余额并在更新之前,另一会话可以更改帐户余额-这是经典的比赛条件。

2.我的IF和UPDATE语句看起来很奇怪。有没有更好的方法来更新正确的[帐户]行?

您链接到的另一个问题没有提供任何基于触发器的解决方案,这有很好的理由。旨在保持非规范化结构同步的触发代码对于正确进行正确的测试非常棘手。甚至具有多年经验的非常高级的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;

下面的测试代码插入10,000个随机交易:

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工具,我在32个线程上以良好的性能,没有死锁和正确的结果运行了该测试100次。除了学习练习之外,我仍然不建议这样做。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.