创建自己的资源管理器

 
资源管理器
首先我们说一下什么是资源,对我们程序员来说,这并不陌生,我们编程的时候就知道了释放资源、减少资源耗用的道理,数据、文件、邮件、消息等都可以是资源,广义上来说,我们计算机中的东西都是资源,管理这些东西的软件都可以称为资源管理器,但这个范围太大了,我们现在讨论的资源管理器是狭义的,符合一定要求的软件,最初它是由微软提出的概念。
微软有一个习惯,就是当它开始重视某项技术,或者把某项技术包装后用于新的领域,就会给这项技术起一个新的名字,比如对于com技术,它创造了OCX, ActiveX, OLE等名字。资源管理器(Resource Manager,简称RM)也一样,当com+着重在企业应用上时,微软认为数据库的概念太狭小了,数据的存储和管理并不见得都是关系数据库,这样,RM的概念就出来了:一种管理持久数据或者资源的软件,支持两阶段提交((two phase commit,简称2PC),在Windows平台上,就是必须支持com+自动事务。
常见的资源管理器如数据库系统Sql Server、Oracle,消息系统MSMQ等。我们在某些需求要求的情况,也必须创建自己的资源管理器,后面我会进行详细介绍,但我们现在更重要的是要了解一个基本的概念:事务的两阶段提交。只有清晰的理解了这个理论知识,我们才可以进行后面的介绍,否则,保证你会一头雾水! 概念的掌握是最基础的,”名不正则言不顺”,这是我的一个经验。
两阶段提交协议
我们平时涉及的大多数事务都是存在与一个关系数据库中,用ADO或者ADO.net提供的事务支持完全可以实现,或者把事务放到数据库一层中,写入到存储过程中,这样可以获得更高的性能。但在企业级开发中,我们面临的是复杂的需求和许多个应用的集成。如果参与事务的是几个数据库系统,而且里面有Sql Server,有Oracle,有DB2,每种数据库系统都存在一个另外的服务器上,那么这种事务就复杂多了,它被称为分布式事务。
2PC就是针对分布式事务处理的,在分布式事务中,参与事务的每个资源管理器负责本身的数据锁定、提交和恢复,但因为参与的是同一个事务,根据事务的原子性要求,要么全部完成,要么全部放弃,它们不能各行其是,必须要相互沟通信息,统一思想,以共同合作完成事务的提交或者回滚。由于不同的数据库由不同的厂家拥有,按照业界发展惯例,要一种协议推出来解决兼容性问题,那就是两阶段提交协议。
两阶段提交的微软版本是OLE事务协议,另外还有X/Open创建的XA协议,参与com+事务的资源管理器需要支持OLE事务协议,某些旧版本的数据库系统不支持OLE事务,而支持XA,但只要它们的驱动程序支持OLE规范和XA规范的相互翻译,仍可以参与的COM+事务中。
在这里,读者很可能已经想到了DTC,即Distributed Transaction Coordinator,它就是微软平台上负责事务协调工作、支持两阶段提交的软件。在分布式事务中,它扮演了关键的角色。每个RM所在的机器上都有一个事务协调器,参与事务的可能有若干个事务协调器,最终形成为一个树状的层次结构,根事务协调器一般是在启动事务的那台机器上。它负责整个事务的指挥工作,是唯一有权限做出最终提交指令的事务协调器。就像元帅指挥每个将军一样,事务协调器指挥每个资源管理器,而将军们(RM)负责具体的各自的事务执行。
 
下面我们就来考察一下两阶段提交的过程,你会发现,它既简单,又精妙。可以说是前辈们的一个杰作。
假定一个事务T开始于机器S,此机器上的事务协调器为C。当客户端代码运行到提交指令时,开始两阶段提交的过程。主要经历如下的阶段:准备阶段和提交阶段。
第一阶段,C首先把<prepare T>指令记录到日志中,然后向其他参与者发送prepare(T)消息,当其他的参与者一旦收到这样的消息,它检查自己应该提交还是放弃,如果自己没有运行成功,则首先把<no T>记录到日志,然后返回abort(T)消息,如果运行一切正常,则记录<ready T>,把消息ready(T)返回给C。
第二阶段,当C接收到所有的应答后,或者时间超出一个预先定义的时间段后,C就可以决定事务的命运了:提交或者回滚。如果所有的应答都是ready(T),则事务进行提交;否则,事务必须回滚。根据这个表决,指令<commit T>或者<abort T>将被记录到日志中,到此为止,这个事务的命运已经铁板定钉了。跟着,C会发出commit(T)或者abort(T)消息给所有的参与者,这些参与者一旦接受到这样的消息,首先还是记录到日志中,然后依令执行。
一个参与者可能在它发送ready(T)消息之前的任何时间,因为某种原因(如突然瘫痪)而导致事务的回滚,消息ready(T)其实就是它对C所作出的承诺,一旦发出这个消息,那么它就能保证它所管辖的事务肯定可以回滚或者提交。我们可以看出,所有的事务协调器在进行任何动作之前,都会先在日志中记录下来,这样,就可以在机器瘫痪或者其他灾难恢复时继续它的任何动作,关于灾难恢复,我们在后面会进行详细的说明。
 
两阶段提交示意图
 
根据上面讨论的两阶段提交协议,我们可以得出一个典型的RM需要处理的事情:
l          当客户访问RM控制的数据时,RM判断是否带有事务上下文,如果是则RM会把自己加入这个事务中,并把要访问的数据复制一份,锁定真实的数据。
l          当客户要更改数据时,RM把这个更改记录到日志文件,并在备份的数据上执行更改(真实数据保持不变)。
l          如果DTC发来准备(prepare)的信号,则RM回放所有的更改,作好提交的一切准备。
l          如果DTC发来提交(commit)的信号,则RM把所有的更改应用到真实的数据上。
l          如果DTC发来回滚(abort)的信号,则RM销毁日志,销毁备份数据。
 
我们可以看出,开发这样一个软件并不容易,如维护一个日志文件的代码可能就很难编写。但有的时候客户的需求并不迁就我们,确实需要某些资源和数据库系统一起加入到一个事务中,比如我们要在插入一条数据库记录的同时上传到某个地方一个xml文件;比如我们要在数据库更改的同时发送一封邮件给管理员,比如,我所开发过的某个系统,要求上传一个PPT文档,保存到数据库中,同时要把此文档的所有页面存为图片,放到服务器上的某个目录下。这种需求虽不多见,但还时常出现,像以往一样,我们希望有人为我们做好基础架构,并简化我们自己的工作。幸运的是,的确有这样的好事,那就是COM+中的补偿资源管理器。
介绍CRM
CRM为实现一个RM提供了大量的标准实现,虽然它不能为您完成所有的任务,但却可以使你的任务相对简单的多(知足吧!)。如果你需要某些不在数据库系统中的资源,如文件等也和数据库一起参与到一个事务中去,那么,毫无疑问,你首先可以想到用CRM机制来开发。
使用CRM开发,你需要实现两个不同的组件:CRM工作者(CRM Worker)和CRM补偿器(CRM Compensator)。工作者执行你应用所需要的所有工作,并把操作记录到日志文件中;补偿器读入你创建的日志文件,根据DTC发来的信息进行提交和回滚。两个组件相互协作,完成事务的提交和回滚。打个比方,其实工作者像一个职员,他做具体的工作,而补偿器更像老板一样,它具有决定权!来判断所有的工作是否最终成立。工作的分配可以让职员做多数的工作,但也不能教条,有的时候老板自己要动手做更多的工作,职员反而比较空闲,这个由他们协商(其实就是你做主),把工作作好是第一要务!
还有一个角色是办事员(Clerk),是com+为我们提供的一个工具对象,它实现了IcrmLogControl接口,可以操作日志文件(这是对我们普通程序员来说比较难的部分)。我们的工作者正是通过办事员的WriteLogRecord方法来写日志文件的,工作者也是通过办事员的RegisterCompensator方法来注册它要用的补偿器。
工作者在执行每步的操作时,都必须先在日志文件中记录这个操作,至于怎样记录这个操作,完全由开发者来决定,WriteLogRecord的参数本来就是object类型的对象,你可以自己决定参数的类型,可以是一个简单的字符串,也可以是自己定义的复杂的对象。只要补偿器读取日志时加一些识别代码能够读出即可。
CRM补偿器是一个实现了IcrmCompensator接口的对象,接口里面有一个SetLogControl方法,它可以获取办事员来读写日志记录(注意,也可以写日志!)。在事务的准备阶段,DTC会通知办事员,由办事员调用补偿器的BeginPrepare、PrepareRecord和EndPrepare方法。如果事务要提交,则调用BeginCommit,CommitRecord和EndCommit方法,如果事务要回滚,则调用BeginAbort、AbortRecord和EndAbort方法。
 
 

  1

CRM工作原理图
 
补偿器的运行不在开发者的控制中,它可能马上被调用,也可能在未来某个时间被调用。更让我们惊奇的是,在事务的准备阶段调用的补偿器实例和在事务提交时调用的补偿器实例可能不是同一个实例。也就是说,我们的工作者完成任务后,后面的工作交给事务协调器来处理了,我们是无能为力了!事务协调器会在合适的时间内激活我们的补偿器进行工作。补偿器根据事务协调器发来的信息进行工作,可能先是准备信号,然后是提交信号,也可能根本不发过来准备信号(其他RM执行时候出错),直接就是回滚信号。
 
我们来解释一下补偿器每个方法的调用。
准备阶段:
BeginPrepare() 通知补偿器进入准备阶段,补偿器可以在这里做他需要做的任何事情。
PrepareRecord(LogRecord rec) 具体执行准备,针对日志文件中的每条记录调用一次,参数rec即是日志文件中的一条记录。返回值是bool类型,提供补偿器删除日志记录的机会,如果返回值是true,则当前记录会从日志中删掉。某些记录可以只传递到准备阶段,来做一些额外的工作,然后把它从日志中移出。
EndPrepare() 调用此方法表明准备阶段即将结束,这里补偿器有最后的机会来放弃事务:返回false表示它没有准备好,这样将会导致事务回滚。
 
提交:
这个阶段方法的调用和准备阶段相似,但是有一点我们必须清楚,DTC调用此方法时用的实例可能和准备阶段一样,可能是灾难恢复后读取日志文件重新生成的实例,所以不要在这两个阶段之间用实例变量传递信息!
BeginCommit(bool fRecovery) 调用此方法表明开始提交,参数fRecovery表明是否是灾难恢复后的提交,如果是true,则补偿器可能在完成准备阶段后突然系统崩溃或者网络瘫痪。
CommitRecord(LogRecord rec) 具体执行提交,针对日志文件中的每条记录调用一次,利用返回值为true来从日志文件中删掉此记录,一般应该返回true,因为如果返回false则可能导致以后重复运行此操作。
即便这样,仍然有可能导致一个操作的重复运行,比如执行某个操作时,将要返回true的哪个瞬间,系统又崩溃了,下次恢复后,由于日志文件中关于这个操作的记录仍存在,所以会再一次执行。CRM要求我们的操作是”幂等(dempotence)操作”,那就是无论执行多少次,结果都是一样。比如我们记录下来的日志应该是”让某帐户上的钱增加为1000元”,而不是”给某帐户增加50元”!
EndCommit() 调用此方法时,表示提交结束。
 
回滚:
回滚可能发生在任何阶段,甚至准备阶段还没开始的时候。所以我们要清楚准备、提交、回滚都是独立的,不要设置什么信息的传递。
促使补偿器执行回滚的原因可能是:
l         客户组件调用 SetAbort() 方法;
l          在准备阶段, CRM 崩溃或者调用EndPrepare方法时返回false;
l          在准备阶段,其他的RM报告错误或者不能访问;
l          接受到DTC回滚的信号;
l          事务超时;
l          被人工强制回滚
 
BeginAbort(bool fRecovery) 通知补偿器要开始回滚。参数fRecovery表示是否是灾难恢复后的回滚。
AbortRecord(LogRecord rec) 具体执行回滚,针对日志的每条记录执行一次,返回true将从日志中删掉此记录。
EndAbort() 通知回滚结束。
 
 
一个CRM的例子
     我们要做一个分布式事务,在数据库中更改一条记录,同时创建一个文件,并把它复制到另外一个目录。数据库用的是 Sql server中的Northwind ,操作文件的RM由我们利用CRM开发。代码在Framework1.1,Windows 2000 Server上测试通过。
在利用COM+容器来开发net组件时候,我们要进行如下的工作:
(一)使所有的类从System.EnterpriseServices.ServicedComponent直接或间接继承,System.EnterpriseServices命名空间包含了所有企业服务的类库,在.net上,把COM+这些服务称为企业服务。
(二) 创建程序集,编写代码。
(三) 创建强名字。
(四)根据需要,在程序集层或者类层设置属性。
 
需要注意,在程序集层上必须有这两个属性 :
[assembly: ApplicationCrmEnabled],它表明这个程序集需要“启用补偿资源管理器”。
 [assembly: ApplicationActivation(ActivationOption.Server)],它表明必须为“服务器应用程序”。
 
如下是我们的工作者类,我们随便起个名字: Worker,它主要包括一个Test方法,完成创建文件、复制文件的动作。
[AutoComplete]
public void Test(string fileName,string desFile)
{
// 创建我们的办事员
            Clerk clerk = new Clerk(typeof(CRMCompensator), "CRMCompensator", CompensatorOptions.AllPhases);
            // 记录日志,用一个数组作参数,“C”表示创建文件
            clerk.WriteLogRecord(new string[]{"C",fileName});
            // 把日志保存到磁盘上
            clerk.ForceLog();
            // 创建文件,并写一些文本
            using (StreamWriter writer=new StreamWriter(fileName,false,System.Text.Encoding.GetEncoding("GB2312")))
            {  
                writer.WriteLine("测试数据");
                writer.Close();
            }
            // 自己写的一个记录跟踪信息的方法,记录到事件查看器中
            LogMessage.WriteLog("create finish","");
            // 记录日志,用一个数组作参数,“M”表示复制文件
            clerk.WriteLogRecord(new string[]{"M",desFile});
            clerk.ForceLog();
            File.Copy(fileName,desFile,true); // 复制到另外一个目录
 
            LogMessage.WriteLog("Test finish","");
}
以上代码可以看出,我们要做的工作都在这里做完了,确实生成了文件、确实复制了文件。如果事务最终提交,这些工作结果都不会变,也不用做其他的工作了,但如果最终事务回滚了,则我们需要把我们做的一切结果给抹掉,恢复原来的一切状态,至于是不是不留痕迹,那要看你自己的功力了!
我们的补偿器代码如下,为了可以更清楚的理解,我给出整个类代码,你就不用担心什么地方遗漏使你的程序运行不起来了哦。
public class CRMCompensator:Compensator
    {
        public override void BeginPrepare()
        {
            LogMessage.WriteLog("BeginPrepare","");
        }
     
        public override bool PrepareRecord(LogRecord rec)
        {
            LogMessage.WriteLog("PrepareRecord","");
            return false; 
        }
     
        public override bool EndPrepare()
        {
            LogMessage.WriteLog("EndPrepare","");
            return true;
        }
     
        public override void BeginCommit(bool fRecovery)
        {
            LogMessage.WriteLog("BeginCommit","");
        }
        //针对日志文件中的每条记录被调用一次
        public override bool CommitRecord(LogRecord rec)
        {
            LogMessage.WriteLog("CommitRecord","");
            string[] d =(string[])rec.Record;
            LogMessage.WriteLog(d[0],"");
 
            return true; //删除记录
        }
     
        public override void EndCommit()
        {
            LogMessage.WriteLog("EndCommit","");
        }
     
        public override void BeginAbort(bool fRecovery)
        {
            LogMessage.WriteLog("BeginAbort","");
        }
        //针对日志文件中的每条记录被调用一次
        public override bool AbortRecord(LogRecord rec)
        {
            LogMessage.WriteLog("AbortRecord","");
 
            string[] d =(string[])rec.Record;
            LogMessage.WriteLog(d[0],"");
 
            if(d[0]=="C")
            {
                string fileName=d[1];
                if(System.IO.File.Exists(fileName))
                    System.IO.File.Delete(fileName);
            }
            else if(d[0]=="M")
            {
                string fileName=d[1];
                if(System.IO.File.Exists(fileName))
                    System.IO.File.Delete(fileName);
            }
            return true;
        }
     
        public override void EndAbort()
        {
            LogMessage.WriteLog("EndAbort","");
        }
    }
 
我们看一下另一个 RM即关系数据库的代码,类名为DBTest,它进行一个数据库操作,并调用我们的CRM类,整个事务在此开始。
        [AutoComplete]
        public void Test(string fileName,string desFile)
        {
            //先调用我们的CRM组件
            crmtest.Worker crm=new crmtest.Worker();
            crm.Test(fileName,desFile);
 
            SqlConnection myConnection = new SqlConnection("server=localhost;user id=sa;password=;database=NorthWind;");
            //没什么意义,只是为了测试,便于观察
            SqlCommand mySqlCleanup = new SqlCommand("update [order details] set quantity=quantity+1 where orderID=10248 and productID=11 ", myConnection);
 
            try
            {
                myConnection.Open();
                mySqlCleanup.ExecuteNonQuery(); 
            }
            catch(Exception)
            {
                LogMessage.WriteLog("Sql error","");
                ContextUtil.SetAbort();
            }
            finally
            {
                myConnection.Close();
            }
        }
    }
注意,工作者类 Worker和数据库类DBTest必须在事务中,需要加如下的属性:
[SecurityRole("CrmTestDemoRole", SetEveryoneAccess = true)]
    [Transaction(TransactionOption.Required)]
这些属性只是说明类需要事务支持,并且进行了适当的权限设置,如果你不太清楚它的含义,请参阅 MSDN。
 
最后是客户端代码 ,比较简单,但要注意客户端程序需要引入System.EnterpriseServices.dll
DBTest b=new DBTest();
b.Test(“C://crmtest.txt”,” d://crmtest2.txt”);
 
以上代码需要注意几点:
1.                我们并没有在补偿器的提交方法里做任何事情,因为这些事情在工作者中已经实际做完了,我们只是在回滚方法里把生成的文件删除掉,这样仍然实现了操作的原子性。当然,我们可以把创建文件和复制文件延迟到补偿器的提交方法里做,没什么问题,依据你的实际应用,工作本来是两个对象协作完成的。
2.                注意在工作者注册补偿器的时候有一个参数: CompensatorOptions.AllPhases,这个参数表示补偿器参与事务的哪些阶段,它还有如下的值:
        CompensatorOptions.AbortPhase 回滚阶段
         CompensatorOptions.CommitPhase 提交阶段
         CompensatorOptions.PreparePhase 准备阶段
         CompensatorOptions.FailIfInDoubtsRemain 如果进入未决状态则选择失败
         如果选择AllPhases则包含AbortPhase 、CommitPhase 、PreparePhase 三个阶段。
3.      编译完成第一次运行时,组件会自动注册到 Com+应用环境中。调试过程中我们可以查看Windows事件查看器,来得到跟踪记录。这是比较简单的跟踪,.net中有专门用于监控事务恢复活动的工具类.
 
最后,如果需求变为要求在事务中删除某个文件,该怎么办呢?开动脑子,不要做教条主义者!我们可以让工作者先把文件移到临时一个目录中,然后在补偿器的提交方法里,把临时目录中的文件删掉,在补偿器的回滚方法里,把临时目录中的文件移回到原来目录。总之,依据不同的需求,你可以很灵活的开发。
进一步的分析
  
1. 隔离性(Isolation)
        有一个遗憾的地方是CRM并没有为我们实现隔离性,这需要我们自己实现。这一点很重要。一般地,资源管理器会锁住一些参与事务的资源,如数据库系统,他提供了行锁定,这样在事务发生期间,这些被锁住的数据不会被其他的事务更改。
        在某些情况下,有两个同时运行的事务,它们都用到了你的补偿器,我们的补偿器不能像数据库系统那样,可以跟踪两个事务ID,我们的资源完全可能被每个事务交替更改,更改的结果也就不是我们所希望的那样了,最后的数据是不确定的。
        你可以选择你的处理方案,如文件锁定,或者其他形式的锁定。你只要记住,你的CRM对象不是单子,你不能确定同时有几个事务在运行!
2. 灾难恢复
事务技术的一个重要的亮点是对于灾难的处理,事务要保证在任何不可预测的情况下,都有完备的应对措施,数据都能得到正确的处理。我们这里讨论有三种情况:某台运行RM的机器崩溃;根事务协调器崩溃;网络断掉。
(1)     运行某个RM的机器崩溃,在它恢复后,检查它的日志文件:
l          如果包含<commit T>指令,则进行提交。
l          如果包含<abort T>指令,则进行回滚。
l          如果包含<ready T>指令,则它试图和根事务协调器C联系,如果C在运行,则取得相应的信息(提交或者回滚)依令而行;如果C也崩溃了,则它试图联系参与事务的其他机器,发出一个query-status(T)消息来询问,这些机器会检查自己的日志文件,把事务的最后命运返回给它;如果这些机器也不知道事务的命运,那么它就不知道怎么办了,该提交还是放弃呢?只能等根事物协调器恢复后才能知道,这种状态为未决(In Doubt)状态。
l          如果没有以上三个指令,那么我们可以知道它在发出应答Prepare询问之前崩溃了,因为它不可能再次发出应答,所以事务必须回滚。
(2)     根事务协调器崩溃,那么所有的参与者需要自己推测事务的命运:
l          如果某个参与者的日志中有<commit T>记录,那么事务肯定是提交
l          如果某个参与者的日志中有<abort T>记录,那么事务肯定是回滚
l          如果某个参与者日志中没有<ready T>记录,这说明根事务协调器C还没有决定要提交事务,所以选择直接回滚。
l          如果以上三种情况都不是,那么一定是所有的参与者日志中都包含<ready T>指令,但又不知道是否该提交,也即未决状态
(3)     网络断掉,参与事务的几台机器之间的网线断掉,分为几种情况:
l          如果发生在所有参与者应答ready(T)消息之前,则事务回滚。
l          如果发生在所有参与者应答ready(T)消息之后,但都还没有接收到下一步指令时候,进入未决状态。
l          如果发生在所有参与者接收到指令后,则不影响事务的进行。
 
对于未决状态,如果补偿器注册时被设置了FailIfInDoubtsRemain则进行回滚,否则将再次和事务协调器联系,来试图得到指令;管理员也可以在COM+组件管理器内强制提交和回滚。
      我们可以看出,在恢复时,CRM 机制的首要工作是寻找那个相应的日志文件,在找到日志文件后,然后根据记录的日志来进行下一步的工作。那个日志文件一般存在于C:/WINNT/system32/DTCLog目录中,初始大小为1MB.文件名类似” {3A874665-3ACC-4B9D-9FCE-B162EA4F1A29}.crmlog”。找到后将创建相应的补偿器实例,来进行恢复工作。如果你在调试 CRM程序,你要取消上一个未完成的事务,可以简单的把这个目录下的相应的文件删除即可。
 
小结
利用CRM可以开发我们需要的资源管理器,使某些不具有事务性的资源也能参与到COM+事务中来。CRM为我们开发资源管理器做了大量的基础工作,我们需要自己实现两个对象:工作者和补偿器,工作者完成我们的具体工作,补偿器被DTC调用,完成提交或者回滚操作。工作者应该设置为”需要事务”,或者”需要新的事务”。
CRM并不能为我们提供隔离性,基于CRM的开发,我们需要自己处理隔离性,另外,开发者要注意每步的操作要求是幂等的,即无论执行多少次结果都是一样。
CRM组件需要被加载com+应用程序中,并且必须为”服务器应用程序”类型,且设置为”启用补偿资源管理器”。事务组件的同步、及时激活(JIT)被Com+默认支持。
 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值