被要求不使用事务并使用一种解决方法来模拟一个

For*_*est 45 sql-server t-sql transaction

我已经开发 T-SQL 好几年了,并且一直在深入挖掘,继续尽我所能学习语言的各个方面。我最近开始在一家新公司工作,并收到了我认为关于交易的奇怪建议。永远不要使用它们。相反,使用模拟事务的解决方法。这是来自我们的 DBA,他在一个数据库中工作,有很多事务,随后有很多阻塞。我主要工作的数据库没有遇到这个问题,我看到过去曾使用过事务。

我知道阻塞是预期的交易,因为这样做是他们的本性,如果你可以不使用它而逃脱,一定要这样做。但是我有很多情况下每个语句都必须成功执行。如果一个失败,他们都必须失败。

我总是尽可能缩小我的事务范围,总是与 SET XACT_ABORT ON 结合使用,并且总是在 TRY/CATCH 内。

例子:

CREATE SCHEMA someschema;
GO


CREATE TABLE someschema.tableA
(id   INT NOT NULL IDENTITY(1, 1) PRIMARY KEY, 
 ColA VARCHAR(10) NOT NULL
);
GO

CREATE TABLE someschema.tableB
(id   INT NOT NULL IDENTITY(1, 1) PRIMARY KEY, 
 ColB VARCHAR(10) NOT NULL
); 
GO


CREATE PROCEDURE someschema.ProcedureName @ColA VARCHAR(10), 
                                          @ColB VARCHAR(10)
AS
SET NOCOUNT, XACT_ABORT ON;
BEGIN
BEGIN TRY
    BEGIN TRANSACTION;

    INSERT INTO someschema.tableA(ColA)
    VALUES(@ColA);

    INSERT INTO someschema.tableB(ColB)
    VALUES(@ColB);

--Implement error
    SELECT 1/0 

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@trancount > 0
    BEGIN
        ROLLBACK TRANSACTION;
    END;
    THROW;
    RETURN;
END CATCH;
END;
GO

Run Code Online (Sandbox Code Playgroud)

这是他们建议我做的。

GO



CREATE PROCEDURE someschema.ProcedureNameNoTransaction @ColA VARCHAR(10), 
                                                       @ColB VARCHAR(10)
AS
SET NOCOUNT ON;
BEGIN
BEGIN TRY
    DECLARE @tableAid INT;
    DECLARE @tableBid INT;

    INSERT INTO someschema.tableA(ColA)
    VALUES(@ColA);
    SET @tableAid = SCOPE_IDENTITY();

    INSERT INTO someschema.tableB(ColB)
    VALUES(@ColB);
    SET @tableBid = SCOPE_IDENTITY();

--Implement error
    SELECT 1/0 

END TRY
BEGIN CATCH
    DELETE FROM someschema.tableA
    WHERE id = @tableAid;

    DELETE FROM someschema.tableB
    WHERE id = @tableBid;

    THROW;

    RETURN;
END CATCH;
END;
GO
Run Code Online (Sandbox Code Playgroud)

我向社区提出的问题如下。作为交易的可行解决方法,这是否有意义?

根据我对事务的了解以及所提出的解决方案,我的观点是,不,这不是一个可行的解决方案,并且会引入许多故障点。

在建议的解决方法中,我看到发生了四个隐式事务。try 中的两个插入,然后是 catch 中删除的两个事务。它确实“撤消”了插入,但不回滚任何内容,因此实际上没有回滚任何内容。

这是一个非常基本的例子来展示他们所建议的概念。我一直在这样做的一些实际存储过程使它们变得非常长且难以管理,因为在这个例子中“回滚”多个结果集与两个参数值变得​​非常复杂,你可以想象。由于“回滚”现在是手动完成的,因此有机会因为真实而错过某些东西。

我认为存在的另一个问题是超时或断开的连接。这仍然会回滚吗?这是我对为什么应该使用 SET XACT_ABORT ON 的理解,以便在这些情况下,事务将回滚。

感谢您提前反馈!

mus*_*cio 63

你不能没有在SQL Server中(也可能是其他任何适当的RDBMS)使用事务。在没有显式事务边界 ( begin transaction... commit)的情况下,每个 SQL 语句都会启动一个新事务,该事务在语句完成(或失败)后隐式提交(或回滚)。

将自己作为“DBA”的人建议的事务模拟无法确保事务处理的四个必要属性中的三个,因为它只解决“软”错误,不能处理“硬”错误,如网络断开、断电、磁盘故障等。

  • 原子性:失败。如果在您的伪事务中间的某处发生“硬”错误,则更改将是非原子的。

  • 一致性:失败。从上面可以看出,您的数据将在“硬”错误后处于不一致状态。

  • 隔离:失败。并发伪事务可能会在您的伪事务完成之前更改由您的伪事务修改的某些数据。

  • 持久性:成功。您所做的更改是持久的,数据库服务器将确保;这是您同事的方法唯一不能搞砸的事情。

锁是一种广泛使用且在经验上成功的方法,可确保所有类型或 RDBMS 中的事务的 ACIDity(此站点就是一个示例)。我发现一个随机的 DBA 不太可能为并发问题想出一个比数百甚至数千过去一直在构建一些有趣的数据库系统的计算机科学家和工程师更好的解决方案,什么,50?60岁?(我意识到这作为“诉诸权威”的论点有些荒谬,但无论如何我都会坚持下去。)

总之,如果可以,请忽略您的“DBA”的建议,如果您有这种精神,请与之抗争,并在出现特定的并发问题时回到这里。


Mic*_*een 14

有一些错误非常严重,以至于永远不会进入 CATCH 块。从文档

严重性为 20 或更高的错误会停止会话的 SQL Server 数据库引擎任务处理。如果发生严重性为 20 或更高的错误并且数据库连接没有中断,TRY...CATCH 将处理该错误。

注意,例如客户端中断请求或断开的客户端连接。

当系统管理员使用 KILL 语句结束会话时。

...

阻止批处理运行的编译错误,例如语法错误。

由于延迟名称解析而发生的错误。

其中很多很容易通过动态 SQL 生成。您所展示的撤消语句不会保护您的数据免受此类错误的影响。

  • 是的——如果没有别的,客户端在执行代码时死亡将构成一个错误“如此严重以至于永远不会进入 CATCH 块”。无论您多么信任软件(不仅仅是您自己的代码,而是所涉及的所有软件堆栈的每个部分),始终存在硬件故障的可能性(同样,可能是链条上的任何地方)让您在任何时候都感到冷淡. 牢记这一点可以很好地抵御导致这种“变通方法”的善意想法。 (2认同)
  • 此外,您可能是死锁的受害者。您的 CATCH 块会运行,但如果它们尝试写入数据库则会抛出。 (2认同)

小智 11

i-one:向您建议的解决方法使(至少)违反ACID 的“A”成为可能。例如,如果远程客户端正在执行 SP 并且连接中断,则可能会发生部分“提交”/“回滚”,因为服务器可以在两次插入/删除之间终止会话(并在它结束之前中止 SP 执行) .

作为交易的可行解决方法,这是否有意义?

dan-guzman:不,在CATCH查询超时的情况下永远不会执行块,因为客户端 API 取消了批处理。没有事务,SET XACT_ABORT ON不能回滚当前语句以外的任何内容。

tibor-karaszi:您有 4 个事务,这意味着更多的日志记录到事务日志文件。请记住,每个事务都需要同步写入到该点的日志记录,即当使用许多事务时,从这方面也会得到更差的性能。

rbarryyoung:如果他们遇到很多阻塞,那么他们要么需要修复他们的数据设计,合理化他们的表访问顺序,或者使用更合适的隔离级别。他们假设他们的问题(以及未能理解它)将成为您的问题。来自数百万其他数据库的证据表明它不会。

此外,他们试图手动实现的实际上是穷人的乐观并发。相反,他们应该做的是使用一些世界上最好的乐观并发,它们已经内置于 SQL Server 中。这就进入了上面的隔离点。他们很可能需要从他们当前使用的任何悲观并发隔离级别切换到乐观并发隔离级别之一, SNAPSHOT或者READ_COMMITTED_SNAPSHOT. 这些将有效地与他们的手动代码做同样的事情,除了它会正确地做。

ross-presser:如果您的流程运行时间非常长——比如今天和下周发生的事情必须跟进,如果下周的事情失败了,那么今天的事情必须追溯失败——你可能想要研究sagas。严格来说,这是在数据库之外,因为它需要一个服务总线。


use*_*855 5

坏主意代码只会更昂贵修复线。

如果使用显式事务(回滚/提交)存在阻塞问题,请将您的 DBA 指向 Internet,以获得一些解决问题的好主意。

这是一种帮助缓解阻塞的方法:https : //www.sqlservercentral.com/articles/using-indexes-to-reduce-blocking-in-concurrent-transactions

索引减少了必须在表/页中查找行/行集的查找次数。它们通常被视为一种减少 SELECT * 查询执行时间的方法,也是正确的。它们被认为不适合涉及大量 UPDATES 的表。事实上,在这些情况下发现 INDEXES 是不利的,因为它们增加了完成 UPDATE 查询所需的时间。

但情况并非总是如此。稍微深入研究 UPDATE 语句的执行,我们发现它也涉及首先执行 SELECT 语句。这是一种特殊且常见的场景,其中查询更新互斥的行集。与流行的看法相反,此处的 INDEXES 可以显着提高数据库引擎的性能。


bob*_*lux 5

这不是一个编程问题,而是一个人际/沟通不畅的问题。您的“DBA”很可能担心锁,而不是事务。

其他答案已经解释了为什么你必须使用事务...我的意思是这就是 RDBMS 所做的,如果没有正确使用事务,就没有数据完整性,所以我将重点关注如何解决真正的问题,即:找出原因您的“DBA”对事务产生了过敏,并说服他改变主意。

我认为这个人混淆了“糟糕的代码导致糟糕的性能的特定场景”和“所有事务都是糟糕的”。我不认为一个有能力的 DBA 会犯这样的错误,所以这真的很奇怪。也许他对一些糟糕的代码有过非常糟糕的经历?

考虑这样的场景:

BEGIN
UPDATE or DELETE some row, which takes locks it
...do something that takes a while
...perform other queries
COMMIT
Run Code Online (Sandbox Code Playgroud)

这种事务使用方式持有一个锁(或多个锁),这意味着命中相同行的其他事务将不得不等待。如果锁被持有很长时间,特别是如果许多其他事务想要锁定相同的行,这确实会损害性能。

你可以做的是问他为什么他有这种不使用事务的奇怪错误想法,什么类型的查询有问题等等。然后尝试说服他你肯定会避免类似的糟糕情况,你会监控你的锁使用情况并表现、让他放心等等。

他告诉你的是“不要碰螺丝刀!” 所以您在问题中发布的代码基本上是使用锤子来驱动螺丝。更好的选择是让他相信你知道如何使用螺丝刀......

我可以想到几个例子......好吧,它们是在 MySQL 上,但这也应该可以。

有一个论坛,全文索引需要一段时间才能更新。当用户提交帖子时,事务将更新主题表以增加帖子计数和最后发布日期(从而锁定主题行),然后插入帖子,并且事务将保持锁定直到全文索引完成更新COMMIT 已完成。

由于它运行在 RAM 太少的 rustbucket 上,更新所述全文索引通常会导致盒子中单个缓慢旋转的驱动器上出现几秒钟的密集随机 IO。

问题在于,单击该主题的人会导致查询增加该主题的视图计数,这也需要锁定该主题行。因此,在更新全文索引时,没有人可以查看该主题。我的意思是,可以读取该行,但更新它会锁定。

更糟糕的是,发帖会更新父论坛表上的帖子计数,并在全文索引更新时保持锁定……这会冻结整个论坛几秒钟,并导致大量请求堆积在 Web 服务器队列中。

解决方案是以正确的顺序获取锁:开始,插入帖子并更新全文索引而不获取任何锁,然后使用帖子计数和最后发布日期快速更新主题/论坛行,然后提交。这样就彻底解决了问题。只是围绕几个查询进行操作,非常简单。

在这种情况下,事务不是问题......它是在长时间操作之前获取不必要的锁。在事务中持有锁时要避免的其他示例:等待用户输入、从缓慢旋转的驱动器、网络 IO 等访问大量未缓存的数据。

当然,有时,您别无选择,必须在持有繁琐的锁的同时进行漫长的处理。对此有一些技巧(对数据副本进行操作等),但性能瓶颈通常来自于无意获取的锁,只需对查询进行重新排序即可解决问题。更好的是,了解编写查询时所采取的锁......

我不会重复其他答案,但真的......使用交易。您的问题是说服您的“DBA”,而不是解决数据库最重要的功能......