【问题标题】:How to avoid using duplicate savepoint names in nested transactions in nested stored procs?如何避免在嵌套存储过程中的嵌套事务中使用重复的保存点名称?
【发布时间】:2010-04-09 10:15:30
【问题描述】:

我有一个几乎总是遵循的模式,如果我需要在事务中结束一个操作,我会这样做:

BEGIN TRANSACTION
SAVE TRANSACTION TX

-- Stuff

IF @error <> 0
    ROLLBACK TRANSACTION TX

COMMIT TRANSACTION

过去这对我很有帮助,但是在使用这种模式多年(并复制粘贴上面的代码)之后,我突然发现了一个让我大吃一惊的缺陷。

很多时候,我会有一个存储过程调用其他存储过程,所有这些都使用相同的模式。我发现(以我的成本)是因为我在任何地方都使用相同的保存点名称,所以我可能会遇到我的外部事务被部分提交的情况——这与我试图实现的原子性正好相反.

我整理了一个展示问题的示例。这是一个批次(没有嵌套的存储过程),因此看起来有点奇怪,因为您可能不会在同一批次中两次使用相同的保存点名称,但我的真实场景太令人困惑而无法发布。

CREATE TABLE Test (test INTEGER NOT NULL)

BEGIN TRAN 
SAVE TRAN TX

    BEGIN TRAN
    SAVE TRAN TX
        INSERT INTO Test(test) VALUES (1)
    COMMIT TRAN TX

    BEGIN TRAN
    SAVE TRAN TX
        INSERT INTO Test(test) VALUES (2)
    COMMIT TRAN TX

    DELETE FROM Test

ROLLBACK TRAN TX
COMMIT TRAN TX

SELECT * FROM Test

DROP TABLE Test

当我执行此操作时,它会列出一条记录,其值为“1”。换句话说,即使我回滚了我的外部事务,表中还是添加了一条记录。

发生的事情是外层的ROLLBACK TRANSACTION TX 回滚到内层的最后一个SAVE TRANSACTION TX。现在我把这些都写出来了,我可以看到它背后的逻辑:服务器正在回顾日志文件,将其视为线性事务流;它不理解事务嵌套所隐含的嵌套/层次结构(或者,在我的实际场景中,调用其他存储过程)。

所以,很明显,我需要开始使用唯一的保存点名称,而不是到处盲目地使用“TX”。但是 - 这就是我最终要说到的地方 - 有没有办法以可复制的方式做到这一点,以便我 可以 仍然在任何地方使用相同的代码?我可以以某种方式即时自动生成保存点名称吗?做这种事情是否有惯例或最佳实践?

每次开始交易时想出一个唯一的名称并不难(可以基于 SP 名称或类似名称),但我确实担心最终会出现冲突 - 你不会了解它,因为它只是默默地破坏了您的数据,而不是导致错误... :-(

【问题讨论】:

    标签: sql sql-server tsql transactions


    【解决方案1】:

    同意 KM 的解决方案。

    我更喜欢使用 GUID 来生成唯一的保存点名称。

    DECLARE @savepoint AS VARCHAR(36)
    SET @savepoint = CONVERT(VARCHAR(36), NEWID())
    
    BEGIN TRANSACTION
    SAVE TRANSACTION @savepoint
    ...
    
    ROLLBACK TRANSACTION @savepoint
    COMMIT TRANSACTION
    

    【讨论】:

    • 是的,我可能更喜欢它,因为它的代码看起来不那么难看,而且无论如何拥有一个“可读”的保存点名称并没有真正的好处,因为你从未见过它们。
    • 查看我对 KM 的回复。 @savepoint 中超过 32 的任何字符都将被忽略,留下一个截断的 GUID。
    • NEWID() 长度为 36 个字符。它恰好有4个破折号。如果你 REPLACE(NEWID(), '-', '') 你得到正好 32 个字符并保留 GUID 的唯一性。 :)
    • 值得在答案中添加这个调整吗?特别是,最好将 VARCHAR(36) 更改为 VARCHAR(32) 以确保您无法复制粘贴该代码并忘记 32 个字符的限制...
    【解决方案2】:

    查看文档:SAVE TRANSACTION (Transact-SQL)

    SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable }
    [ ; ]
    

    看起来你可以根据变量命名它,所以尝试制作你的模式:

    DECALRE @savepoint_variable varchar(1000)
    SET @savepoint_variable=OBJECT_NAME(@@PROCID)+'|'+CONVERT(char(23),GETDATE(),121)
    
    BEGIN TRANSACTION
    SAVE TRANSACTION @savepoint_variable
    
    -- Stuff
    
    IF @error <> 0
    BEGIN
        ROLLBACK TRANSACTION @savepoint_variable
    END
    
    COMMIT TRANSACTION
    

    当从不同的过程调用时,您的 @savepoint_variable 将具有不同的本地值,并且您的回滚应该回滚正确的。我在保存点名称中输入了当前日期时间,因为您可能会在某些时候使用递归,如果这是复制粘贴模式,最好处理所有情况。

    【讨论】:

    • 如果您在此答案顶部的链接中进一步阅读,您会看到“@savepoint_variable ... 可以将超过 32 个字符传递给变量,但只有前 32 个字符会使用。”这意味着您的 CONVERT(char(23),GETDATE(),121) 很可能会从您的保存点名称(也可能是您的过程名称的结尾)中被截断。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-04
    • 2018-10-07
    • 1970-01-01
    • 2020-02-22
    • 1970-01-01
    • 2015-06-24
    • 1970-01-01
    相关资源
    最近更新 更多