制作回滚豁免插入
Making a rollback exempt insert
这是一个奇怪的问题:有没有办法将回滚豁免插入 table?
场景如下:
我们有一个可以做事的触发器。
有时,此触发器会调用 RAISERRROR()
。并且外部事务回滚。
但是,在触发器中,我想将值插入到日志记录中 table 并且不让它在回滚期间消失,如果您愿意的话,事务豁免插入。
抓香蕉
您可以使用 try-catch
和 pass/throw 将异常捕获到外部范围,其中必须存在相同的东西。顺便说一句,这允许您收集 call-stack
。如果您仅通过 storedprocs 工作,则可以实施此解决方案。每个过程都必须有这样的模式:
begin try
end try
begin catch
if @@trancount > 0
rollback
insert into <log> (...)
values (...)
throw
end catch
因此最顶层的过程将成功地向日志中插入一行 table。
缺点:
- 层次结构较深 - "dummy" 插入太多要回滚
- 在以下情况下仍然可能不记录任何内容:
- 严重性太高
- SP 不符合此模式
- 有外层/"client-side"事务管理
优点:
- 不完美,但仍然可行,实施起来也不难。
意见反馈:
- 我确实在小项目的生产服务器上使用了这种方法,收集了调用堆栈等等。这是一个后台软件,拥有约 50-100 个在线用户,一些机器人 运行,远远没有 "highload"。它运行良好,帮助解决了许多问题。开销没有打扰任何人。
难以捉摸的强盗
您可以构建一个 CLR 程序集,它确实会插入日志 table,仅此而已……但是!可以指定一个单独的连接,使这个程序集通过这个单独的连接与 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
缺点:
- 可能会导致连接池问题
- 获得简单插入的有点长的方法
- 可能会导致与建立连接和程序集本身相关的其他问题(+权限、所有权等)
优点:
- 这个想法有点令人兴奋
- 可以记录到文件而不是数据库
意见反馈:
- 我不记得我是否在产品上使用过这种方法(尽管我记得这是最近的经历……或者只是为了测试目的而尝试实施)
敲,敲,管理员
这个简单的语句(实际上只是额外的选项WITH LOG
)将把你想要的任何错误信息写入SQL SERVER事件日志:
RAISERROR(...) WITH LOG
这不是应该使用 SQL SERVER 日志的方式,但这是记录重要内容(用于解决问题)的最快方式。可以在 SSMS 代理的 windows.
中查看记录的事件
缺点:
- 可以考虑权限问题
- 搜索和零定制能力差
- dba 会恨你的(当然,如果他检查过服务器日志的话)
优点:
- 单行实现
意见反馈:
- 我只在一家公司遇到过这种登录方式。我想,前段时间它被几个开发人员用来捕获细微的错误,但后来遍布代码并成为 "standard"。因此,一段时间后,几乎不可能在事件日志中找到特定的内容……而且实际上只有少数同事可以访问生产服务器的日志。所以它实际上是 a) 有害的 b) 无用的。所以我一找到时间就把它从系统的托管部分中删除了。
飞翔的荷兰人
{关于基于 DML 触发器构建系统的意见讨论场所}
在我看来,您没有在项目中使用存储过程,而是执行临时查询。如果你有一个带有 ORM 或类似东西的后端应用程序 - 用它写日志。此外,也许这个后端应用程序是一个更好的地方来做你在触发器中做的事情。
如果您的项目是没有 appserver/backend 应用程序的客户端-服务器应用程序,并且您得到的只是一个临时查询和触发器,那么没有太多数据可以记录。没有调用堆栈(在服务器端)。并且很难确定您(用户、应用程序)是如何出现这个特定异常的。所以在这种情况下登录客户端可能更有用。
如果您创建了一个 table 变量,然后插入其中 - 这将不会包含在回滚中,因此您可以将内容转储到永久 table.
例如
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
稍后再写。您可以使用代替触发器:
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 行受影响)
这是一个奇怪的问题:有没有办法将回滚豁免插入 table?
场景如下: 我们有一个可以做事的触发器。
有时,此触发器会调用 RAISERRROR()
。并且外部事务回滚。
但是,在触发器中,我想将值插入到日志记录中 table 并且不让它在回滚期间消失,如果您愿意的话,事务豁免插入。
抓香蕉
您可以使用 try-catch
和 pass/throw 将异常捕获到外部范围,其中必须存在相同的东西。顺便说一句,这允许您收集 call-stack
。如果您仅通过 storedprocs 工作,则可以实施此解决方案。每个过程都必须有这样的模式:
begin try
end try
begin catch
if @@trancount > 0
rollback
insert into <log> (...)
values (...)
throw
end catch
因此最顶层的过程将成功地向日志中插入一行 table。
缺点:
- 层次结构较深 - "dummy" 插入太多要回滚
- 在以下情况下仍然可能不记录任何内容:
- 严重性太高
- SP 不符合此模式
- 有外层/"client-side"事务管理
优点:
- 不完美,但仍然可行,实施起来也不难。
意见反馈:
- 我确实在小项目的生产服务器上使用了这种方法,收集了调用堆栈等等。这是一个后台软件,拥有约 50-100 个在线用户,一些机器人 运行,远远没有 "highload"。它运行良好,帮助解决了许多问题。开销没有打扰任何人。
难以捉摸的强盗
您可以构建一个 CLR 程序集,它确实会插入日志 table,仅此而已……但是!可以指定一个单独的连接,使这个程序集通过这个单独的连接与 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
缺点:
- 可能会导致连接池问题
- 获得简单插入的有点长的方法
- 可能会导致与建立连接和程序集本身相关的其他问题(+权限、所有权等)
优点:
- 这个想法有点令人兴奋
- 可以记录到文件而不是数据库
意见反馈:
- 我不记得我是否在产品上使用过这种方法(尽管我记得这是最近的经历……或者只是为了测试目的而尝试实施)
敲,敲,管理员
这个简单的语句(实际上只是额外的选项WITH LOG
)将把你想要的任何错误信息写入SQL SERVER事件日志:
RAISERROR(...) WITH LOG
这不是应该使用 SQL SERVER 日志的方式,但这是记录重要内容(用于解决问题)的最快方式。可以在 SSMS 代理的 windows.
中查看记录的事件缺点:
- 可以考虑权限问题
- 搜索和零定制能力差
- dba 会恨你的(当然,如果他检查过服务器日志的话)
优点:
- 单行实现
意见反馈:
- 我只在一家公司遇到过这种登录方式。我想,前段时间它被几个开发人员用来捕获细微的错误,但后来遍布代码并成为 "standard"。因此,一段时间后,几乎不可能在事件日志中找到特定的内容……而且实际上只有少数同事可以访问生产服务器的日志。所以它实际上是 a) 有害的 b) 无用的。所以我一找到时间就把它从系统的托管部分中删除了。
飞翔的荷兰人
{关于基于 DML 触发器构建系统的意见讨论场所}
在我看来,您没有在项目中使用存储过程,而是执行临时查询。如果你有一个带有 ORM 或类似东西的后端应用程序 - 用它写日志。此外,也许这个后端应用程序是一个更好的地方来做你在触发器中做的事情。
如果您的项目是没有 appserver/backend 应用程序的客户端-服务器应用程序,并且您得到的只是一个临时查询和触发器,那么没有太多数据可以记录。没有调用堆栈(在服务器端)。并且很难确定您(用户、应用程序)是如何出现这个特定异常的。所以在这种情况下登录客户端可能更有用。
如果您创建了一个 table 变量,然后插入其中 - 这将不会包含在回滚中,因此您可以将内容转储到永久 table.
例如
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
稍后再写。您可以使用代替触发器:
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 行受影响)