Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1035240
  • 博文数量: 243
  • 博客积分: 3053
  • 博客等级: 中校
  • 技术积分: 2975
  • 用 户 组: 普通用户
  • 注册时间: 2009-05-02 21:11
文章分类

全部博文(243)

文章存档

2013年(2)

2012年(20)

2011年(5)

2010年(114)

2009年(102)

我的朋友

分类: WINDOWS

2012-04-16 22:46:02

何为自治事务( Autonomous Transaction )?

简单的说,如果在会话中调用一个过程,则此过程会运行在此会话当前的事务作用域中;而自治事务可以让这个过程运行在一个独立的事务作用域中,使用其 不受外部事务(也就是调用此过程的会话的当前事务)的影响,可以被独立的回滚 / 提交。
为了方便理解,举个例子:

use tempdb
go
if object_id ( 'emps' ) is not null
  drop table emps;
go
create table emps ( id int identity , names varchar ( 20), salary numeric ( 8, 2));
go
insert into emps( names, salary) values ( 'Tom' , 3000.00);
insert into emps( names, salary) values ( 'Jerry' , 5000.00);
go

if object_id ( 'emp_logs' ) is not null
  drop table emp_logs;
go
create table emp_logs ( id int identity , actions varchar ( 20), timestamps datetime , username varchar ( 128));
go
emps 表用于记录员工信息, emp_logs 表用于记录对 emps 的修改动作。

如果现在要记录所有对 emps 表上 salary 列的修改操作,即使此操作没有成功提交。为了实现此功能,可以创建一个触发器用于捕获 update 语句的执行,
create trigger trg_emps_update_salary
on emps for update
as
  if update ( salary)
  insert into emp_logs( actions, timestamps, username) values ( 'UPDATE SALARY' , getdate (), suser_name ());
go

正常情况下,这个触发器没有任何问题。但是,如果 update 语句被回滚或没有被成功提交,触发器将不会向 emp_logs 表中插入任何记录,这是因为触发器和激发此触发器的 update 语句都位于一个事务作用域中,总是被一起提交 / 回滚。
那么,在触发器中使用显式事务( begin tran   insert into ... commit tran )怎么样?同样不行,因为如同 SQL Server 的嵌套事务,只有最外层的 commit tran 语句才会真正提交之前的所有修改,而在内层事务中的 commit tran (触发器中的)将不会起任何作用。

解决方法:
我们知道,在 ORACLE 中,可以在触发器中指定 PRAGMA AUTONOMOUS_TRANSACTION 选项让其支持自治事务。那么,在 SQL Server 下要如何处理?
这里提供了两种方法:
其一, 通过 loopback 服务器链接执行远过程调用, 让此过程拥有独立的事务作用域(适用于 SQL Server 2008 )。具体步骤,
建立 LOOPBACK 服务器链接
EXEC sp_addlinkedserver @server = N'loopback' , @srvproduct = N' ' , @provider = N'SQLNCLI' , @datasrc = @@SERVERNAME
设置服务器链接选项,阻止 SQL Server 由于远过程调用而将本地事务提升为分布事务 (重点)
EXEC sp_serveroption loopback, N'rpc out' , 'TRUE'
EXEC sp_serveroption loopback, N'remote proc transaction promotion' , 'FALSE'
将 insert 语句打包为一个过程
use tempdb
go
create procedure usp_insert_emplogs
as
  insert into emp_logs( actions, timestamps, username) values ( 'UPDATE SALARY' , getdate (), suser_name ());
go

在触发器中通过 LOOPBACK 服务器链接调用此存储过程
create trigger trg_emps_update_salary
on emps for update
as
  if update ( salary)
  exec loopback. tempdb . dbo. usp_insert_emplogs;
go

现在,测试一下吧

begin tran
  update emps set salary=salary*1.1 where id=1;
rollback tran

其二 , 通过 CLR 存储过程实现(适用于 SQL Server 2005 及以上版本),通过 loopback 连接让 CLR 过程运行在一个独立的事务作用域下。具体步骤,
创建 CLR 运行库(AutoTran.dll )
/*
  编译命令(System.Transactions 需要添加对 System.Transactions.dll 库文件的引用)
  csc.exe /r:c:/WINDOWS/Microsoft.NET/Framework/v2.0.50727/System.Transactions.dll /target:library AutoTran.cs
*/

using System;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Transactions;

public class AutoTran
{
    [SqlProcedure]
    public static void ExecuteATStmt(SqlString sql, SqlString dbname, SqlInt16 iso)
    {
         // 创建不加入环境事务的事务作用域
        using (TransactionScope trans = new TransactionScope(TransactionScopeOption.Suppress))
        {
            using (SqlConnection conn = new SqlConnection("context connection=true" ))
            {
                conn.Open();
                 // 获取过程调用者的安全上下文
                SqlCommand cmd = conn.CreateCommand();
                cmd.CommandText = "select suser_name()" ;
                string loginame = (string )cmd.ExecuteScalar();

                 // 创建 LOOPBACK 连接(禁止使用连接池)
                using (SqlConnection connAT =
                  new SqlConnection("server=.;integrated security=true;database=master;pooling=false" ))
                {
                    connAT.Open();
                    connAT.ChangeDatabase((string )dbname);
                    SqlCommand cmdAT = connAT.CreateCommand();

                    // 定义自治事务的事务隔离等级
                    string level;
                    switch ((Int16 )iso)
                    {
                        case 1:
                            level = "read uncommitted" ;
                            break ;
                        case 3:
                            level = "repeatable read" ;
                             break ;
                        case 4:
                            level = "serializable" ;
                            break ;
                        case 5:
                            level = "snapshot" ;
                            break ;
                         default :
                            level = "read committed" ;
                            break ;
                    }
                    cmdAT.CommandText = "set transaction isolation level " + level;
                    cmdAT.ExecuteNonQuery();
                     / / 将 CLR 存储过程运行的安全上下文切换到调用者的,从而避免 CLR 过程运行在 SQL Server 服务进程的安全上下文中
                    cmdAT.CommandText = "execute as login = '" + loginame + "' with no revert" ;
                    cmdAT.ExecuteNonQuery();

                    // 执行 SQL 语句
                    cmdAT.CommandText = (string )sql;
                    cmdAT.ExecuteNonQuery();
                }
            }
            trans.Complete();
        }
    }
}

CLR 存储过程代码:
use master
go
exec sp_configure 'show advanced options' , 1;
go
reconfigure
go
exec sp_configure 'clr enabled' , 1;
go
reconfigure
go
alter database master set trustworthy on ;
go

if object_id ( 'dbo.ExecuteATStmt' ) is not null
  drop procedure dbo. ExecuteATStmt;
go

if exists ( select * from sys.assemblies where name = 'AutoTran' )
  drop assembly AutoTran;
go

create assembly AutoTran authorization dbo
from 'C:/Devs/Projects/clrAutoTran/clrAutoTran/bin/Release/clrAutoTran.dll'
with permission_set= external_access;
go


create procedure dbo. ExecuteATStmt
  @stmt nvarchar ( max ), @dbname nvarchar ( 128)= 'master' , @iso smallint = 2
as external name AutoTran. AutoTran. ExecuteATStmt;
go

 

 

演示:

use tempdb
go

if object_id ( 'emps' ) is not null
  drop table emps;
go

create table emps ( id int identity , fname varchar ( 20), salary numeric ( 8, 2));
go

insert into emps ( fname, salary) values ( 'Tom' , 4000);
insert into emps ( fname, salary) values ( 'Jerry' , 6000);
go

if object_id ( 'emp_logs' ) is not null
  drop table emp_logs;
go
create table emp_logs ( id int identity , loginame sysname , spid int , stmt nvarchar ( 4000));
go

-- 触发器用于捕获 / 记录所用对 EMPS 表 salary 列的更新操作

if object_id ( 'trg_emps_update' ) is not null
  drop trigger trg_emps_update;
go
create trigger trg_emps_update on emps
with execute as owner 、
for update as
  set nocount on ;
if update (salary)
begin
  declare @event table ( EventType nvarchar ( 30), Parameters int , EventInfo nvarchar ( 4000));
  insert into @event exec ( 'dbcc inputbuffer(@@spid) with no_infomsgs' );
  declare @stmt nvarchar ( max );
  set @stmt= N'insert into emp_logs(loginame,spid,stmt) ' +
  N'select ''' + original_login()+ ''',' + ltrim ( @@spid )+ ',' ;
  select @stmt= @stmt+ '''' + replace ( EventInfo, '''' , '''''' )+ '''' from @event;
  exec master. dbo. ExecuteATStmt @stmt, 'tempdb' ;
end
go

 

-- 测试,即使 UPDATE 动作被回滚,也会产生记录

begin tran
  declare @salary varchar ( 8);
  set @salary= '10000' ;
  update emps set salary= salary;
rollback tran

select * from emp_logs;

阅读(3472) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

校长的马夹2012-04-17 15:52:44

博主写的很精髓啊,小弟受教了~~