简体   繁体   English

类似事务的编程风格或明智的异常处理

[英]Transaction-like style of programming or the wise exception handling

Question by abstract example通过抽象示例提问

Suppose you have 2 methods: DoJob1() , DoJob2() .假设您有 2 个方法: DoJob1()DoJob2() Each of them has transaction-like behavior, that is, either does its job or reports an error.它们中的每一个都有类似事务的行为,也就是说,要么完成它的工作,要么报告一个错误。

How should I write a method which executes DoJob1() then DoJob2() , but is transaction-like itself, that is, guarantees the roll-back of the action performed by DoJob1() in case an error occurs while processing DoJob2() ?我应该如何编写一个执行DoJob1()然后DoJob2()的方法,但它本身是类似事务的,也就是说,如果在处理DoJob2()时发生错误,保证回滚DoJob1()执行的操作?

Of course, you are free to choose the way of error handling (bool return value, real exceptions, global error variable — you name it).当然,您可以自由选择错误处理的方式(bool 返回值、真正的异常、全局错误变量——随您便)。

Background背景

The idea is to write (some) methods transaction-like.这个想法是编写(一些)类似交易的方法。 And when an exception occurs, suggest the user to repeat the 'transaction'.并且当异常发生时,建议用户重复“交易”。

I have a thought on the possible approach to the problem, which I am going to post in a while;我对解决这个问题的可能方法有一个想法,稍后我会发布; (in order not to limit your imagination) (为了不限制你的想象力)

In general I do:一般来说,我这样做:

transactionalJob1()
    transaction_begin()
    doJob1()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob2()
    transaction_begin()
    doJob2()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob1And2()
    transaction_begin()
    doJob1()
    doJob2()
    transaction_end()
    exception:
        transaction_rollback()

If your language of choice supports template methods you may wrap it all up.如果您选择的语言支持模板方法,您可以将其全部打包。

My thoughts我的想法

I have identical idea.我有相同的想法。

Here is a beta approach (C#):这是一种测试方法 (C#):

class Program
{
    Random rnd = new Random();

    void DoJob1()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("First case");

        Console.WriteLine("First job done.");
    }

    void DoJob1Rollback()
    {
        Console.WriteLine("First job rollback.");
    }

    void DoJob2()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("Second case");

        Console.WriteLine("Second job done.");
    }

    void DoJob2Rollback()
    {
        Console.WriteLine("Second job rollback.");
    }

    void Run()
    {
        bool success = false;

        while (true)
        {
            try
            {
                TransactHelper.DoTransaction(
                    DoJob1,
                    DoJob2
                );
                success = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine("--------------------------------------");
                Console.WriteLine("Exception: " + ex.Message);
                // Console.WriteLine("Stack trace:");
                // Console.WriteLine(ex.StackTrace);
                // Console.WriteLine();
            }

            if (!success)
            {
                // ask the user for another chance
                Console.Write("Retry? ");
                if (Console.ReadLine().ToLower() != "yes")
                    break;
            }
            else
                break;
        }

        Console.WriteLine("Batch job {0}", success ? "succeeded." : "did not succeed.");

        Console.WriteLine("Press Enter to exit.");
        Console.ReadLine();
    }

    static void Main(string[] args)
    {
        (new Program()).Run();
    }
}

This looks as nice, as running TransactHelper.DoTransaction这看起来和运行 TransactHelper.DoTransaction 一样好

class TransactHelper
{
    public static void DoTransaction(params ThreadStart[] actions)
    {
        int i = 0;
        int n = actions.Length;

        // exception to pass on
        Exception ret_ex = null;

        // do the list of jobs
        for (; i < n; ++i)
        {
            try
            {
                ThreadStart ts = actions[i];
                ts();
            }
            catch (Exception ex)    // register exception
            {
                ret_ex = ex;
                break;
            }
        }

        if (ret_ex != null)         // exception registered, rollback what's done
        {
            int k = i;              // method which failed
            for (; i >= 0; --i)
            {
                MethodInfo mi = actions[i].Method;
                string RollbackName = mi.Name + "Rollback";

                // set binding flags - the same as the method being called
                BindingFlags flags = (mi.IsStatic) ? BindingFlags.Static : BindingFlags.Instance;
                if (mi.IsPrivate)
                    flags |= BindingFlags.NonPublic;
                if (mi.IsPublic)
                    flags |= BindingFlags.Public;

                // call rollback
                MethodInfo miRollback = mi.DeclaringType.GetMethod(RollbackName, flags);
                miRollback.Invoke(actions[i].Target, new object[] { });
            }

            throw new TransactionException("Transaction failed on method " + actions[k].Method.Name, ret_ex);
        }
    }
}

[global::System.Serializable]
public class TransactionException : Exception
{
    public TransactionException() { }
    public TransactionException(string message) : base(message) { }
    public TransactionException(string message, Exception inner) : base(message, inner) { }
    protected TransactionException(
      System.Runtime.Serialization.SerializationInfo info,
      System.Runtime.Serialization.StreamingContext context)
        : base(info, context) { }
}

However, there are pitfalls.然而,也有陷阱。

  • Additional pain by maintaining two methods instead of one通过维护两种方法而不是一种方法来增加痛苦
  • What if an exception occurs in a xRollback method?如果 xRollback 方法出现异常怎么办? (however, it is the same situation as it occurs in the catch{} block) (然而,这与在 catch{} 块中发生的情况相同)
  • (Current design runs only methods without arguments — this is easily extended) (当前设计仅运行没有 arguments 的方法——这很容易扩展)

I've initially thought on this problem as I worked in a team which produces plenty of code like:当我在一个产生大量代码的团队工作时,我最初想到了这个问题:

_field = new Field();
_field.A = 1;
_filed.B = "xxx";

instead of代替

Field tmp = new Field();
tmp.A = 1;
tmp.B = "xxx";

_field = tmp;

The beauty of the transaction are:交易的美妙之处在于:

  • data integrity (!)数据的完整性 (!)
  • you can suggest the user to repeat the action您可以建议用户重复该操作

PS聚苯乙烯

Maybe I'm trying to re-invent the wheel?也许我正在尝试重新发明轮子? Is there a library for a wiser approach?有没有更明智的方法的图书馆? What are the pitfalls of this design I didn't foresee?这个设计有哪些我没有预见到的缺陷?

The second approach I undertook is just to log roll-back actions in a separate class.我采用的第二种方法是在单独的 class 中记录回滚操作。

class Transaction
{
    IList<Action> _rollBacks = new List<Action>();

    public void AddRollBack(Action action)
    {
        _rollBacks.Add(action);
    }

    public void Clear()
    {
        _rollBacks.Clear();
    }
    public void RollBack()
    {
        for (int i = _rollBacks.Count - 1; i >= 0; --i)
        {
            _rollBacks[i]();
            _rollBacks.RemoveAt(i);
        }
    }
}

The actual code is written in pairs, eg:实际代码是成对写的,eg:

File.Move(file, file + "__");
string s = (string) file.Clone();
tr.AddRollBack(() => File.Move(s + "__", s));

When I need to do something transactionally , I create a transaction object, and cover sensitive code into try... catch... finally当我需要做一些交易时,我创建一个交易 object,并将敏感代码覆盖到try... catch... finally

Transaction tr = new Transaction();

UpdateLogic ul = new UpdateLogic();
ul.Transaction = tr;

// ........ more initialization (of classes)

try
{
    // .... more initialization, which is a part of transaction
    ul.DoPreparation();
    ul.DoCopying();

    tr.Clear();                         // at this point most of update is ok

    ul.DoCleanup();

    ShowMessage("Update completed", "Update completed successfully.", false);
}
catch (Exception ex)
{
    // handel error
}
finally
{
    // show message in UI
    try
    {
        tr.RollBack();
    }
    catch (Exception ex)
    {
        ShowMessage("Error while performing rollback of actions", ex.Message, true);
    }

    // ... update is done        
}

It is very convenient, because this guarantees integrity of files.这非常方便,因为这保证了文件的完整性。 With wise coding, the integrity is maintained even if the process is killed in between of doing things or rolling back (yet leaving some trash in filesystem).通过明智的编码,即使进程在做事或回滚之间被杀死(但在文件系统中留下一些垃圾),完整性也能得到维护。

That depends on what kind of work the method is supposed to do.这取决于该方法应该做什么样的工作。 In general, first the transaction's side-effects are stored into a temporary place, and then when the transaction commits, the side-effects are stored to a permanent place in one atomic operation.通常,首先将事务的副作用存储到一个临时位置,然后在事务提交时,将副作用存储到一个原子操作中的永久位置。 The details of how to do that are very different depending on whether you are modifying something in the file system (read from some books that how database transaction logs work), some in-memory data structure, something over the.network or something else.具体如何做的细节是非常不同的,这取决于你是在修改文件系统中的东西(从一些书中读到数据库事务日志是如何工作的)、内存中的数据结构、.net 上的东西还是其他东西。

For example, I've once written a transactional in-memory key-value database (part of http://dimdwarf.sourceforge.net/ ).例如,我曾经写过一个事务性内存中键值数据库( http://dimdwarf.sourceforge.net/的一部分)。 It keeps a temporary list of all modifications done during the transaction.它保留了一个临时列表,其中包含在事务期间所做的所有修改。 Then when the transaction commits, the modified keys are locked in the database, the modifications are stored in the database (this operation must not fail), after which the keys are unlocked.然后在事务提交时,将修改后的键锁定在数据库中,将修改保存在数据库中(此操作一定不能失败),之后将键解锁。 Only one transaction may commit at a time, and no other transaction can see the changes until the transaction has fully committed.一次只能提交一个事务,在该事务完全提交之前,其他事务无法看到更改。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM