架構之路(9):Session Per Request

發表於2016-08-20

本系列:

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

 

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是一個開銷很大的過程。

 引入sessionFactory

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

 EndSession

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

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

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

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

 按需獲取session

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

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

 

總是使用顯性事務

如果同學們檢視原始碼,就會發現,我們的session總是啟用了事務。

 總是使用事務

在我們傳統的觀念中,使用“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生成情景。

程式碼非常簡單,如下:

 呼叫EndSession()

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

同時,為了避免反覆的宣告,我們提取出BaseController,由所有Controller繼承,並在BaseController上宣告SessionPerRequest即可:

 SessionPerRequest宣告

 

其他

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

 AJAX返回JsonResult

 

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

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

相關文章