2010-10-24 66 views
11

當您使用Session-Per-Request模式時,您在使用NHibernate的3層應用程序中使用哪種模式/體系結構,需要支持重試事務失敗? (因爲異常後ISession變爲無效,即使這是死鎖或超時或活鎖異常)。如何讓NHibernate在每個請求使用會話時重試死鎖事務?

+0

你如何處理你的問題? – 2011-02-09 12:16:37

+0

看到我的答案.. – Henrik 2011-02-10 12:33:59

回答

33

注意2現在,我絕不會把寫事務放到Web項目中 - 而是使用消息傳遞+隊列,並在後臺處理消息,以便導致事務性工作完成。

但是,我仍然會使用交易進行閱讀以獲得一致的數據;以及來自web項目的MVCC /快照隔離。在這種情況下,你會發現session-per-request-per-transaction是完全正確的。

注1這篇文章的想法已被放置在Castle Transactions framework和我的新NHibernate Facility

好的,這裏是一般的想法。假設您想爲客戶創建未完成的訂單。你有某種圖形用戶界面,例如瀏覽器/ MVC應用程序,創造與相關信息的新的數據結構(或者你從網絡訪問此數據結構):

[Serializable] 
class CreateOrder /*: IMessage*/ 
{ 
    // immutable 
    private readonly string _CustomerName; 
    private readonly decimal _Total; 
    private readonly Guid _CustomerId; 

    public CreateOrder(string customerName, decimal total, Guid customerId) 
    { 
     _CustomerName = customerName; 
     _Total = total; 
     _CustomerId = customerId; 
    } 

    // put ProtoBuf attribute 
    public string CustomerName 
    { 
     get { return _CustomerName; } 
    } 

    // put ProtoBuf attribute 
    public decimal Total 
    { 
     get { return _Total; } 
    } 

    // put ProtoBuf attribute 
    public Guid CustomerId 
    { 
     get { return _CustomerId; } 
    } 
} 

你需要的東西來處理它。這可能是某種服務總線中的命令處理程序。 「命令處理程序」這個詞就是其中之一,你可以稱它爲「服務」或「域服務」或「消息處理程序」。如果你正在進行函數式編程,那將是你的消息框實現,或者如果你在做Erlang或Akka,它將是一個Actor。

class CreateOrderHandler : IHandle<CreateOrder> 
{ 
    public void Handle(CreateOrder command) 
    { 
     With.Policy(IoC.Resolve<ISession>, s => s.BeginTransaction(), s => 
     { 
      var potentialCustomer = s.Get<PotentialCustomer>(command.CustomerId); 
      potentialCustomer.CreateOrder(command.Total); 
      return potentialCustomer; 
     }, RetryPolicies.ExponentialBackOff.RetryOnLivelockAndDeadlock(3)); 
    } 
} 

interface IHandle<T> /* where T : IMessage */ 
{ 
    void Handle(T command); 
} 

以上顯示了您可能爲給定問題域(應用程序狀態/事務處理)選擇的API用法。

隨着執行:

static class With 
{ 
    internal static void Policy(Func<ISession> getSession, 
             Func<ISession, ITransaction> getTransaction, 
             Func<ISession, EntityBase /* abstract 'entity' base class */> executeAction, 
             IRetryPolicy policy) 
    { 
     //http://fabiomaulo.blogspot.com/2009/06/improving-ado-exception-management-in.html 

     while (true) 
     { 
      using (var session = getSession()) 
      using (var t = getTransaction(session)) 
      { 
       var entity = executeAction(session); 
       try 
       { 
        // we might not always want to update; have another level of indirection if you wish 
        session.Update(entity); 
        t.Commit(); 
        break; // we're done, stop looping 
       } 
       catch (ADOException e) 
       { 
        // need to clear 2nd level cache, or we'll get 'entity associated with another ISession'-exception 

        // but the session is now broken in all other regards will will throw exceptions 
        // if you prod it in any other way 
        session.Evict(entity); 

        if (!t.WasRolledBack) t.Rollback(); // will back our transaction 

        // this would need to be through another level of indirection if you support more databases 
        var dbException = ADOExceptionHelper.ExtractDbException(e) as SqlException; 

        if (policy.PerformRetry(dbException)) continue; 
        throw; // otherwise, we stop by throwing the exception back up the layers 
       } 
      } 
     } 
    } 
} 

正如你所看到的,我們需要一個新的工作單位;每當出現問題時,ISession都會發生。這就是爲什麼循環位於Using語句/塊的外部。具有函數相當於擁有工廠實例,除了我們直接調用對象實例,而不是調用它的方法。它使得更好的調用者API不可用。

我們想要相當平滑地處理我們如何執行重試,所以我們有一個接口,可以由不同的處理程序實現,稱爲IRetryHandler。應該可以將這些內容鏈接到您希望實施控制流程的每個方面(是的,它非常接近AOP)。與AOP的工作方式類似,返回值用於控制控制流,但僅以真/假方式控制,這是我們的要求。

interface IRetryPolicy 
{ 
    bool PerformRetry(SqlException ex); 
} 

AggregateRoot,PotentialCustomer是一個具有生命期的實體。這就是你將用你的* .hbm.xml文件/ FluentNHibernate進行映射。

它有一個與發送的命令對應1:1的方法。這使得命令處理程序完全顯而易見。此外,使用鴨子打字的動態語言,它將允許您將命令的類型名稱映射到方法,類似於Ruby/Smalltalk的方法。

如果你正在做事件採購,事務處理將是類似的,除了事務不會接口NHibernate的。推論是您將保存通過調用CreateOrder(十進制)創建的事件,併爲您的實體提供重新從商店中讀取已保存事件的機制。

最後需要注意的是,我重寫了我創建的三個方法。這是NHibernate方面的一個要求,因爲它需要一種知道實體何時與另一個實體相同的方式,是否應該成套/包裝。更多關於我的執行here。無論如何,這是示例代碼,我不關心我的客戶,現在,所以我沒有實現它們:

sealed class PotentialCustomer : EntityBase 
{ 
    public void CreateOrder(decimal total) 
    { 
     // validate total 
     // run business rules 

     // create event, save into event sourced queue as transient event 
     // update private state 
    } 

    public override bool IsTransient() { throw new NotImplementedException(); } 
    protected override int GetTransientHashCode() { throw new NotImplementedException(); } 
    protected override int GetNonTransientHashCode() { throw new NotImplementedException(); } 
} 

我們需要創建重試策略的方法。當然,我們可以用很多方式來做到這一點。在這裏,我將流暢的接口與靜態方法類型相同類型的同一對象的實例組合在一起。我明確實現了接口,因此在流暢接口中沒有其他方法可見。這個接口只使用我下面的'示例'實現。

internal class RetryPolicies : INonConfiguredPolicy 
{ 
    private readonly IRetryPolicy _Policy; 

    private RetryPolicies(IRetryPolicy policy) 
    { 
     if (policy == null) throw new ArgumentNullException("policy"); 
     _Policy = policy; 
    } 

    public static readonly INonConfiguredPolicy ExponentialBackOff = 
     new RetryPolicies(new ExponentialBackOffPolicy(TimeSpan.FromMilliseconds(200))); 

    IRetryPolicy INonConfiguredPolicy.RetryOnLivelockAndDeadlock(int retries) 
    { 
     return new ChainingPolicy(new[] {new SqlServerRetryPolicy(retries), _Policy}); 
    } 
} 

我們需要一個接口來部分完成對流暢接口的調用。這給了我們類型安全。因此,在完成策略配置之前,我們需要兩個解除引用操作符(即'完全停止' - (。)),遠離靜態類型。

internal interface INonConfiguredPolicy 
{ 
    IRetryPolicy RetryOnLivelockAndDeadlock(int retries); 
} 

鏈接策略可以解決。它的實現會檢查它的所有子節點是否繼續返回,並在檢查它時執行其中的邏輯。

internal class ChainingPolicy : IRetryPolicy 
{ 
    private readonly IEnumerable<IRetryPolicy> _Policies; 

    public ChainingPolicy(IEnumerable<IRetryPolicy> policies) 
    { 
     if (policies == null) throw new ArgumentNullException("policies"); 
     _Policies = policies; 
    } 

    public bool PerformRetry(SqlException ex) 
    { 
     return _Policies.Aggregate(true, (val, policy) => val && policy.PerformRetry(ex)); 
    } 
} 

該策略讓當前線程休眠一段時間;有時數據庫過載,並且有多個讀寫器不斷嘗試讀取數據庫實際上是DOS攻擊(查看幾個月前發生的事情,當facebook崩潰時,因爲它們的緩存服務器都在相同的位置查詢其數據庫時間)。

internal class ExponentialBackOffPolicy : IRetryPolicy 
{ 
    private readonly TimeSpan _MaxWait; 
    private TimeSpan _CurrentWait = TimeSpan.Zero; // initially, don't wait 

    public ExponentialBackOffPolicy(TimeSpan maxWait) 
    { 
     _MaxWait = maxWait; 
    } 

    public bool PerformRetry(SqlException ex) 
    { 
     Thread.Sleep(_CurrentWait); 
     _CurrentWait = _CurrentWait == TimeSpan.Zero ? TimeSpan.FromMilliseconds(20) : _CurrentWait + _CurrentWait; 
     return _CurrentWait <= _MaxWait; 
    } 
} 

同樣,在任何基於SQL的優秀系統中,我們都需要處理死鎖。我們不能真正計劃這些,特別是在使用NHibernate的時候,除了保持一個嚴格的事務策略 - 沒有隱式事務;並注意開放會議在視圖。如果您要獲取大量數據,還需要記住笛卡爾產品問題/ N + 1選擇問題。相反,你可能有多重查詢,或HQL的'fetch'關鍵字。

internal class SqlServerRetryPolicy : IRetryPolicy 
{ 
    private int _Tries; 
    private readonly int _CutOffPoint; 

    public SqlServerRetryPolicy(int cutOffPoint) 
    { 
     if (cutOffPoint < 1) throw new ArgumentOutOfRangeException("cutOffPoint"); 
     _CutOffPoint = cutOffPoint; 
    } 

    public bool PerformRetry(SqlException ex) 
    { 
     if (ex == null) throw new ArgumentNullException("ex"); 
     // checks the ErrorCode property on the SqlException 
     return SqlServerExceptions.IsThisADeadlock(ex) && ++_Tries < _CutOffPoint; 
    } 
} 

幫助類使代碼讀得更好。

internal static class SqlServerExceptions 
{ 
    public static bool IsThisADeadlock(SqlException realException) 
    { 
     return realException.ErrorCode == 1205; 
    } 
} 

不要忘了也要處理IConnectionFactory中的網絡故障(通過委託可能通過實施IConnection)。


PS:如果您不只是閱讀,會話每請求是一種破損模式。特別是如果你正在用你正在寫的同一個ISession進行閱讀,並且你沒有在寫操作之前總是預訂這些讀操作。