【问题标题】:Making a rollback exempt insert制作回滚豁免插入
【发布时间】:2018-09-25 21:01:38
【问题描述】:

这是一个奇怪的问题:有没有办法将回滚豁免插入到表中?

这是场景: 我们有一个做事的触发器。

有时,此触发器会调用RAISERRROR()。并且外部事务回滚。

但是,在触发器中,我想将值插入到日志记录表中,并且在回滚期间不让它消失,如果你愿意的话,这是一个事务豁免插入。

【问题讨论】:

  • 您不能提交事务的一部分。嵌套事务是一个神话。 sqlskills.com/blogs/paul/…
  • 您可能会引发 Windows 事件日志中记录的用户定义错误(编号 50000+,严重性 16+)。然后,您可以创建 SQL Server 代理触发的 Alert(检查 Windows 事件日志)并启动执行写入日志表的 T-SQL 代码的 Job

标签: sql-server tsql transactions sql-server-2016-localdb


【解决方案1】:

香蕉渔获

您可以使用try-catch 捕获异常并将它们传递/抛出到外部范围,其中必须存在相同的东西。顺便说一句,这允许您收集call-stack。如果您仅通过存储过程工作,则可以实施此解决方案。每个 proc 都必须有这样的模式:

begin try
end try
begin catch
   if @@trancount > 0
     rollback

   insert into <log> (...)
   values (...)

   throw
end catch

所以最上面的过程会成功地在日志表中插入一行。

缺点:

  • 层次结构较深 - 需要回滚的“虚拟”插入过多
  • 在以下情况下仍然可以不记录任何内容:
    • 严重性太高
    • SP 不符合此模式
    • 有一个外部/“客户端”事务管理

优点:

  • 并不完美,但仍然可行,实施起来也不难。

反馈:

  • 我确实在小型项目的生产服务器上使用了这种方法,确实收集了调用堆栈等。这是一个后台软件,有大约 50-100 个在线用户,一些机器人在运行,远不是“高负载”。它运行良好,帮助解决了许多问题。开销没有打扰任何人。

难以捉摸的强盗

您可以构建一个插入日志表的 CLR 程序集,仅此而已……但是!可以指定一个单独的连接,使这个程序集通过这个单独的连接与 db 一起工作。这意味着 - 在单独的范围内。所以这个程序集的方法是在一个事务中被调用的,但是不管这个事务范围是被执行的。

所以,而不是:

using(SqlConnection connection = new SqlConnection("context connection=true"))

只需指定一个常规连接字符串。调用它后 - 抛出异常并修改 ERROR_STATUS 以避免额外记录相同的错误。

begin try
end try
begin catch
   if @@trancount > 0
     rollback

   if @@ERRROR_STATUS != @done_with_logging
     exec asm.log(...)

   raiserror @err_msg, @severity, @done_with_logging
end catch

缺点:

  • 可能会导致连接池问题
  • 有点冗长的方法来获得一个微不足道的插入
  • 可能会导致与建立连接和程序集本身相关的其他问题(+权限、所有权等)

优点:

  • 这个想法有点令人兴奋
  • 可以记录到文件而不是数据库

反馈:

  • 我不记得我是否在 prod 上使用过这种方法(尽管我记得这是最近的一次经历......或者只是为了测试目的而尝试实现它)

敲门,敲门,管理员

这个简单的语句(实际上只是附加选项WITH LOG)会将您想要的任何错误消息写入 SQL SERVER 事件日志:

RAISERROR(...) WITH LOG

这不是应该使用 SQL SERVER 日志的方式,但这是记录重要内容(用于解决问题)的最快方式。记录的事件可以在 SSMS 代理的窗口中查看。

缺点:

  • 可能存在权限注意事项
  • 糟糕的搜索和零自定义功能
  • dba 会恨你的(当然,如果他检查过服务器日志)

优点:

  • 单行实现

反馈:

  • 我只在一家公司遇到过这种登录方式。我想,前段时间它被几个开发人员用来捕捉细微的错误,但后来传播到代码中并成为“标准”。因此,一段时间后,几乎不可能在事件日志中找到特定的内容......实际上只有少数同事可以访问 prod 服务器的日志。所以它实际上是a)有害的b)无用的。因此,我一有时间就将其从系统的托管部分中删除。

飞翔的荷兰人

{基于 DML 触发器构建系统的基于意见的讨论的地方}

在我看来,您没有在项目中使用存储过程,而是执行临时查询。如果你有一个带有 ORM 或类似的后端应用程序 - 用它写日志。此外,也许这个后端应用更适合您在触发器中执行的操作。

如果您的项目是一个没有应用服务器/后端应用程序的客户端-服务器应用程序,而您得到的只是一个临时查询和触发器,那么需要记录的数据并不多。没有调用堆栈(在服务器端)。而且很难确定您(用户、应用程序)是如何遇到这个特殊异常的。因此,在这种情况下,登录客户端可能会更有用。

【讨论】:

  • "banana catch" 和 "knock, knock, admin" 没有涵盖这个特定场景,我们希望在触发器中记录到该表以在它拒绝值时进行捕获。然而,“难以捉摸的强盗”似乎是要走的路,这是我在看到你的建议之前问自己的问题,我认为这是前进的方向。关于“飞翔的荷兰人”,我们主要是基于 sproc 的,尽管有些人更喜欢参数化的批处理 SQL,尽管每个人都应该使用 API,但不是每个人都使用。
【解决方案2】:

如果您创建了一个表变量然后插入其中 - 这将不会包含在回滚中,因此您可以将内容转储到一个永久表中。

例如

declare @tab table (msg varchar(255))

BEGIN TRY

    BEGIN TRANSACTION

    select 1+2

    INSERT @tab values ('first step complete')

    SElect 1/0

    INSERT @tab values ('2nd step complete')

    COMMIT

END TRY

BEGIN CATCH

    ROLLBACK

    SELECT * FROM @tab
END CATCH

【讨论】:

  • 可悲的是,我需要在触发器的调用堆栈深处进行此插入。
【解决方案3】:

稍后再写。您可以使用代替触发器:

create table t
(
i int,
s varchar(10)
)
go

create table t2
(
i int,
s varchar(10)
)
go

create table tlog
(
i int,
s varchar(10)
)
go

alter trigger tt on t
INSTEAD OF INSERT
AS
BEGIN
    rollback transaction
    raiserror ('something went wrong', 16, 2)
    insert tlog (i,s)
    select i, s
    from inserted
END
go

truncate table t
truncate table t2
truncate table tlog
go

select * from t
select * from t2
select * from tlog
go

begin transaction
insert t2 (i,s) values (1, 'abc')
insert t (i,s) values (1, 'abc')
commit transaction
go

select * from t
select * from t2
select * from tlog

给出以下输出:

我是


(0 行受影响)

我是


(0 行受影响)

我是


(0 行受影响)

(1 行受影响) 消息 50000,级别 16,状态 2,程序 tt,第 7 行 出了点问题

(1 行受影响) 消息 3609,第 16 层,状态 1,第 4 行 事务在触发器中结束。该批次已中止。 我是


(0 行受影响)

我是


(0 行受影响)

我是


1 abc

(受影响的 1 行)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-11-28
    • 2019-07-03
    • 1970-01-01
    • 2019-05-05
    • 2021-07-30
    • 2020-11-02
    • 2013-05-05
    相关资源
    最近更新 更多