当实现IEnlistmentNotification接口时,我应该在哪里执行操作?

9

我正在尝试通过实现 IEnlistmentNotification 接口来创建自定义的“资源管理器”。该接口具有以下方法:

  • Prepare() - 准备
  • Commit() - 提交
  • Rollback() - 回滚
  • InDoubt() - 不确定

虽然回滚代码应该放在 Rollback() 方法中,但我不确定应该在哪个方法中实现执行实际操作的代码。是应该放在 Prepare() 中?还是放在 Commit() 中?或者是放在类中的其他自定义方法中,在 TransactionScope 块内部从外部代码调用该方法?

2个回答

11

实际工作应在另一个方法中执行。准备和提交是为了实现两阶段提交机制。

模式如下:

using(var transaction = new TransactionScope())
{
    var rc1 = new ResourceManager();
    rc1.DoWork();
    var rc2 = new ResourceManager();
    rc2.DoWork();
    transaction.Complete();
}

在这个例子中,DoWork 应该执行操作。当退出交易范围时,两个资源管理器的 Prepare 方法都会被调用。如果它们都调用了 enlistment.Prepared();,那么两个管理器的 Commit 方法将被调用。那个提交过程不应该失败!

例如,在处理文件时,DoWork 应该将文件重命名以表示正在处理该文件,然后读取和处理该文件。如果任何一个操作失败,它都应该抛出异常并导致 Rollback 被调用。Rollback 应该将文件重命名回原来的名称。Prepare 可以将文件重命名以表示应该删除该文件,并检查是否允许删除该文件。如果任何一个操作失败,它都应该抛出异常。Commit 然后实际上删除该文件。这不会失败,因为我们已经检查了安全性,但即使失败,也不应抛出异常。

实际上可以在 Prepare 方法中删除该文件并调用 enlistment.Done();。这将表明不再需要调用 Commit。但是问题在于,在删除文件之后,其他资源管理器可能会在其 Prepare 中引发异常。因为你指示你已完成,所以你的回滚方法不会被调用。即使它被调用,你也没有办法恢复你的操作...

我希望这解释了一些事情...


我需要将TransactionScope实例传递给管理器并将管理器注册到事务中吗?还是我该如何注册ResourceManager以便调用Prepare、Commit或Rollback? - Stoyan Dimov
1
在我上面的示例中,ResouceManager本身可以检查Transaction.Current!=null,如果是这种情况,它可以使用Transaction.Current.EnlistDurableTransaction.Current.EnlistVolatile自行注册,以便选择最合适的方式。 - Marc Selis
InDoubt是什么? - JobaDiniz
当事务管理器发出单阶段提交但从您的Commit实现中未收到enlistment.Done()信号时,将调用InDoubt。这使得事务管理器不确定您的提交是否正确执行。因此,它调用您的InDoubt方法,该方法应返回调用enlistment.Done()以再次表示提交成功。有关更多信息,请参见文档 - Marc Selis

2
这里有一些包含实现和单元测试的示例代码。 创建一个基类可以让我专注于所需的操作,而不必在各处处理事务。
public abstract class TransactionCreator : IEnlistmentNotification
{
    protected TransactionCreator()
    {
        System.Transactions.Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
    }

    public void Commit(Enlistment enlistment)
    {
        Complete();
        enlistment.Done();
    }

    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }

    //Don't throw an exception here. Instead call ForceRollback()
    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        try
        {
            Execute();
            preparingEnlistment.Prepared();
        }
        catch (Exception e)
        {
            preparingEnlistment.ForceRollback(e);
        }
    }

    public void Rollback(Enlistment enlistment)
    {
        Revert();
        enlistment.Done();
    }

    public abstract void Execute();
    public abstract void Complete();
    public abstract void Revert();
}

为了测试 IEnlistmentNotification 的实现,我们将会:
- 测试预期的流程
- 模拟在一个对象中Execute方法失败并查看在另一个对象中调用回滚。
(我使用NSubstitute进行模拟,但可以忽略这一点)
[TestFixture]
public class TransactionCreatorTest
{
    [Test]
    public void Test_file_gets_created_on_transaction_complete()
    {
        TransactionCreator creator;

        using (var scope = new TransactionScope())
        {
            creator = Substitute.For<TransactionCreator>();

            scope.Complete();
        }
        creator.Received().Execute();
        creator.DidNotReceive().Revert();
    }

    [Test]
    public void Test_file_gets_does_not_get_created_on_rollback()
    {
        TransactionCreator creator = null;
        try
        {
            using (var scope = new TransactionScope())
            {
                creator = Substitute.For<TransactionCreator>();
                var failed = Substitute.For<TransactionCreator>();
                failed.When(x => x.Execute()).Do(x => { throw new Exception(); });
                scope.Complete();
            }
        }
        catch (TransactionAbortedException ex)
        {
            Console.Out.WriteLine(ex);
        }


        creator.Received().Execute();
        creator.Received().Revert();
    }
}

2
失败的事务将不会调用回滚或撤消操作,因此在调用ForceRollback之前,您必须在Prepare方法中还原该事务。(我不知道为什么...) - Andreas Zita

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接