代码之家  ›  专栏  ›  技术社区  ›  bohdan_trotsenko

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

  •  2
  • bohdan_trotsenko  · 技术社区  · 16 年前

    抽象例题

    假设有两种方法: DoJob1() , DoJob2() 它们中的每一个都有类似事务的行为,也就是说,要么执行其任务,要么报告错误。

    DoJob1() 然后 DoJob2() ,但事务是否与自身类似,即保证回滚执行的操作 以防在处理过程中发生错误 DoJob2()

    当然,您可以自由选择错误处理的方式(bool返回值、实际异常、您命名的全局错误变量)。

    背景

    其思想是编写(一些)类似事务的方法。当发生异常时,建议用户重复“事务”。

    我对解决这个问题的可能方法有一个想法,我一会儿就要发表出来;(为了不限制你的想象力)

    4 回复  |  直到 16 年前
        1
  •  1
  •   Manrico Corazzi    16 年前

    一般来说,我会:

    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()
    

        2
  •  0
  •   bohdan_trotsenko    16 年前

    我的想法

    我有同样的想法。

    以下是测试版方法(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();
        }
    }
    

    这看起来和跑步一样漂亮TransactionHelper.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) { }
    }
    

    然而,也有一些陷阱。

    • 通过保持两种方法而不是一种方法来增加疼痛

    当我在一个团队工作时,我最初考虑过这个问题,这个团队产生了大量的代码,比如:

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

    而不是

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

    交易的好处在于:

    • 数据完整性(!)
    • 您可以建议用户重复该操作

    附笔。

    也许我想重新发明轮子? 有没有一个更明智的方法的图书馆? 我没有预见到这种设计的缺陷是什么?

        3
  •  0
  •   bohdan_trotsenko    16 年前

    我采用的第二种方法是在单独的类中记录回滚操作。

    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);
            }
        }
    }
    

    实际代码是成对编写的,例如:

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

    当我需要做什么的时候 交易性的 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        
    }
    

    它非常方便,因为这保证了文件的完整性。使用wise编码,即使进程在执行操作或回滚之间被终止(但在文件系统中留下一些垃圾),也可以保持完整性。

        4
  •  0
  •   Esko Luontola    16 年前

    这取决于该方法应该做什么工作。一般来说,首先将事务的副作用存储到临时位置,然后在事务提交时,在一个原子操作中将副作用存储到永久位置。具体的操作方法会有很大的不同,这取决于您是在修改文件系统中的某些内容(从一些书籍中读到数据库事务日志是如何工作的)、一些内存中的数据结构、通过网络进行的修改还是其他操作。

    例如,我曾经编写过一个事务性内存键值数据库(属于 http://dimdwarf.sourceforge.net/ ). 它保留一个临时列表,列出事务期间所做的所有修改。然后,当事务提交时,修改后的密钥被锁定在数据库中,修改后的密钥存储在数据库中(此操作不能失败),之后密钥将被解锁。一次只能提交一个事务,在事务完全提交之前,其他事务都不能看到更改。

    推荐文章