文章出處

前面的兩篇反應很差:沒評論沒贊。很傷心啊,為什么呢?搞得我好長一段時間都沒更新了——呵呵,好吧,我承認,這只是我的借口。不過,還是希望大家多給反饋。沒有反饋,我就只能猜了:前面兩篇是不是寫得太“粗”了一點?所以這一篇我們盡量詳細點吧。

 

Session Per Request是什么

這是一個使用NHibernate構建Web項目慣用的模式,相關的文章其實很多。我盡量用我的語言(意思是大白話,但可能不精確)來做一個簡單的解釋。

首先,你得明白什么是session。這不是ASP.NET里面的那個session,初學者在這一點上容易犯暈。這是NHibernate的概念。

  • 如果你對它特別感興趣的話,你可以首先搜索“Unit Of Work”關鍵字,了解這個模式;然后逐步明白:session其實是NHibernate對Unit Of Work的實現。
  • 如果你只想了解一個大概,那么你可以把它想象成一個臨時的“容器”,裝載著從數據庫取出來的entity,并一直記錄其變化。
  • 如果你還是覺得暈乎,就先把它當成一個打開的、活動的數據庫連接吧。

我們都知道數據庫連接的開銷是很大的,為此.NET還特別引入了“連接池”的概念。所以,如果能有效的降低數據庫的連接數量,對程序的性能將有一個巨大的提升作用。經過觀察和思考,大家(我不知道究竟是誰最先提出這個概念的)覺得,一個HTTP request分配一個數據庫連接是一個很不錯的方案。于是Session Per Request就迅速流行起來,幾乎成為NHibernate構建Web程序的標配。

 

為什么又要考慮性能了

我《性能》篇發布了以后,雖然贊數很多,但評論區中爭議也還是很大的。但一是評論區后來歪樓了,二是一句話翻來覆去的講太沒意思了,所以我沒有再分辨。但Session Per Request就是一個很好的例子,可以說明什么叫做“性能讓位于可維護性”:

  1. 如果為了性能,破壞了代碼的可維護性,那么我們寧愿不要性能;
  2. 在能夠保證可維護性的前提下,我們當然應該努力的提高性能;
  3. 較之于在局部(非性能瓶頸處)糾結發力,不如在架構的層面上保證/促進整體性能的提高。

我說提到的“性能的問題先不管”,以及“忘記數據庫”等,是基于矯枉必須過正的出發點,希望能夠有振聾發聵的效果。但結果看來不是很好,評論里我還是看到了“SELECT TOP 1 * FROM TABLE WHERE ID>CURRRID”之類的東西。這說明什么?關系數據庫不但已經在你腦子里扎根,而且已經把你腦子都塞滿了。不是說這樣不行,只是這樣的話,實在沒辦法和你談論面向“對象”。

Session Per Request就是一個已經被廣泛采用,行之有效的,能在架構層面提升性能的一個設計。

 

UI還是Service

我們僅從Session Per Request的定義,什么Http啊,Request啊,憑直覺就能想到UI層的范疇吧?

網上的很多示例都確實是這么寫的。在Application里BuildSessionFactory,在HttpModule中配置:一旦HTTP request到達,就生成一個session;Http request結束,就調用session.flush()同步所有更改。

但是,我們在架構中就已經確立了這樣一個原則:UI層不涉及數據庫操作。更直觀的看,UI層的project連NHibernate.dll的引用都沒有。那怎么辦呢?

現在想來很好笑,當年我可是費了不少的腦細胞:其實只需要在Service層封裝相關的操作,然后在UI層調用Service層即可。

那些把我繞暈了的不靠譜的想法大家可以不用去理會了。如果確實有興趣,可以思考一下:NHibernate中session是有上下文環境(context)的,我們這里當然應該設置成web,但Service最后會被編譯成一個dll,這個dll里能取到HttpContext么?

但在Service里怎么封裝,也是一件值得斟酌的事。

 

變異,些許的性能提高

我最后采用的方案是引入BaseService:

首先,在BaseService中設置一個靜態的sessionFactory;而且,在BaseService的靜態構造函數中給sessionFactory賦值(Build SessionFactory)。這樣,就可以保證SessionFactory只生成一次,因為生成SessionFactory是一個開銷很大的過程。

public class BaseService
{
    private static ISessionFactory sessionFactory;

    static BaseService()
    {
        string connStr = ConfigurationManager.ConnectionStrings["dev"].ConnectionString;
        sessionFactory = Fluently.Configure()
            .Database(
                MySQLConfiguration.Standard.ConnectionString(connStr).Dialect<MySQL5Dialect>())
            .Mappings(ConfigurationProvider.Action)
            .Cache(x => x.UseSecondLevelCache().ProviderClass<SysCacheProvider>())
            .ExposeConfiguration(
                c => c.SetProperty(NHibernate.Cfg.Environment.CurrentSessionContextClass, "web"))
            .BuildSessionFactory();
    }
引入sessionFactory

其次,在BaseService中暴露一個靜態的EndSession()方法,在Request結束時將數據的變化同步到持久層(數據庫)。所以當UI層調用時,不需要實例化一個BaseService,只需要BaseService直接調用即可:

public class BaseService
{
    public static void EndSession()
    {

    }
}
EndSession

然后,我們回頭看看前面的說法:“一旦HTTP request到達,就生成一個session;”,所以理論上需要一個InitSession()的方法,生成/提供一個session。但我突然有了點小聰明:有些頁面可能是不需要數據庫操作的,比如幫助、表單呈現,或者其他我們暫時想不到的頁面。那我們無論如何總是生成一個session,是不是浪費了點?

越想越覺得是這么一回事,所以左思右想,弄出了一個方案:按需生成session。大致的流程是:

  • 嘗試獲取session;
  • 如果“當前環境”中已有一個session,就直接使用該session;
  • 否則就生成一個session,使用該session,并將其存入當前環境中。

看來NHibernate支持這種思路,所以提供了現成的接口,可以很方便的實現上述思路:

        protected ISession session
        {
            get
            {
                ISession _session;
                if (!CurrentSessionContext.HasBind(sessionFactory))
                {
                    _session = sessionFactory.OpenSession();
                    CurrentSessionContext.Bind(_session);
                }
                else
                {
                    _session = sessionFactory.GetCurrentSession();
                }

                return _session;
            }
        }
按需獲取session

其中CurrentSessionContext就是上文所謂的“當前環境”,在我們的系統中國就是一個HttpContext;我們使用GetCurrentSession()就總是能夠保證取出的session是當前HttpContext中已有的session。所有的Service都繼承自BaseService,直接調用BaseService中的session,這樣就可以有效的保證了Session Per Request的實現。

同學們,這下知道了吧?其實我骨子里還是一個很“摳”性能的人。但這樣做究竟值不值?我也不太確定,畢竟這樣做一定程度上增加了代碼的復雜性,而所獲得的性能提升其實有限。

 

總是使用顯性事務

如果同學們查看源代碼,就會發現,我們的session總是啟用了事務。

        protected ISession session
        {
            get
            {
                //......

                if (!_session.Transaction.IsActive)
                {
                    _session.BeginTransaction();
                }

                return _session;
            }
        }

        public static void EndSession()
        {
            if (CurrentSessionContext.HasBind(sessionFactory))
            {
                    //.......
                    using (sessionFromContext.Transaction)
                    {
                        try
                        {
                            sessionFromContext.Transaction.Commit();
                        }
                        catch (Exception)
                        {
                            sessionFromContext.Transaction.Rollback();
                            throw;
                        }
                    }
            }
        }
總是使用事務

在我們傳統的觀念中,使用“transaction”,會增加數據庫的開銷,降低性能。但實際上并不是這樣的,至少我可以保證在NHibernate和Mysql中不是這樣的。

大致的原因有幾點:

  • 即使不顯式的聲明事務,數據庫也會顯式的生成一個事務;
  • NHibernate的二級緩存需要事務做保證

詳細的介紹請參考:Use of implicit transactions is discouraged

其實,既然使用了Session Per Request模式,我們即使從業務邏輯上考慮,也應該總是使用“事務”:很多時候一次表單提交要執行多個數據庫操作,一些步驟執行了一些報了異常,數據不完整咋辦?

 

沒有Session.Save()和Update()

前面已經反復說過,在Service中,沒有數據庫的Update操作。我們是通過:Load()數據 -> 改變其屬性 -> 然后在Save()到數據庫來實現的。

但同學們查看我們的源代碼的時候會發現:“咦?怎么沒有Session.Save()這樣一個過程?”

首先,大家應該了解NHibernat中的Update()不是我們大多數同學想象的那樣,對應著sql里的update語句。它實際上用于多個session交互時的場景,我們目前的系統是永遠不會使用的。

然后,NHibernate也不是使用session.Save()來同步session中的數據到數據庫的。我們系統中只是偶爾使用session.Save()來暫時的獲得entity的Id。

最后,NHibernate中實際上是使用session.Flush()來最終“同步”內存(session)中的數據到數據庫的。而我們代碼中使用的是session.Transaction.Commit(),這會自動的調用session.Flush()。

因為Session Per Request模式,我們在UI層中,總是會在request結束時調用EndSession(),所以在Service的代碼中,看起來就沒有了“存儲”數據的過程。

 

UI層的調用

那么,在UI層的哪里調用EndSession()呢?(因為按需生成session,已經不需要BeginSession()了)

大致來說,有兩種方案,一種是使用HttpModule,另一種是利用ASP.NET MVC的filter機制。

我們采用了后者,一則是這樣更簡單,另一方面是因為:當引入ChildAction之后,從邏輯上講,Session Per Action更自洽一些。比如一個Request可能包含多個Child Action,將多個Child Action放在一個session里,可能出現難以預料的意外情況。

當然,這樣做的不利的一面就是會消耗更多的session,但好在session的開銷很小,而且我們使用的“按需生成session”可以降低一些session生成情景。

代碼非常簡單,如下:

    public class SessionPerRequest : ActionFilterAttribute
    {
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            #if PROD
            FFLTask.SRV.ProdService.BaseService.EndSession();
            #endif

            base.OnResultExecuted(filterContext);
        }
    }
調用EndSession()

#if PROD的使用是為了前后端分離(后文詳述):只有當調用ProdService時才使用以上代碼,UI開發人員使用UIDevService時不需要改項操作。

同時,為了避免反復的聲明,我們提取出BaseController,由所有Controller繼承,并在BaseController上聲明SessionPerRequest即可:

    [SessionPerRequest]
    public class BaseController : Controller
    {
    }
SessionPerRequest聲明

 

其他

由于我們在Action呈現后實現數據的同步(session.Transaction.Commit()),所以我們所有的Ajax調用,沒有使用Web API,而是繼承自ActionResult的JsonResult。否則,不會觸發OnResultExecuted事件,也無法同步數據庫。

        public JsonResult GetTask(int taskId)
        {
            string title = _taskService.GetTitle(taskId);
            return Json(new { Title = title });
        }
AJAX返回JsonResult

 

 

綜上,我們實際上是借鑒了SessionPerRequest的思路,實際上采用了按需生成Session、且一個Action使用一個session的實現。可以描述成:SessionPerActionIfRequire,呵呵。

通過SessionPerRequest,我們可以發現架構的一個重要作用:將系統中“技術復雜”的部分封裝起來,讓開發人員可以脫離復雜瑣碎的技術,而專注于具體業務的實現。事實上,采用我們的系統,即使一個不怎么懂NHibernate的普通開發人員,經過簡單的介紹/培訓,也可以迅速的開始業務領域代碼的編寫工作。

 

+++++++++++++++++++++++++++++

應該是2016年春節前最后一篇《架構之路》的更新了,先預祝大家新春快樂,萬事如意!

另外,歡迎各種留言評論(包括拍磚)。    

O(∩_∩)O~

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()