本系列:
- 架構之路(1):目標
- 架構之路(2):效能
- 野生程式設計師:優先招聘
- 架構之路(4):測試驅動
- 架構之路(5):忘記資料庫
- 架構之路(6):把框架拉出來
- 架構之路(7):MVC點滴
- 架構之路(8):從CurrentUser說起
前面的兩篇反應很差:沒評論沒贊。很傷心啊,為什麼呢?搞得我好長一段時間都沒更新了——呵呵,好吧,我承認,這只是我的藉口。不過,還是希望大家多給反饋。沒有反饋,我就只能猜了:前面兩篇是不是寫得太“粗”了一點?所以這一篇我們儘量詳細點吧。
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就是一個很好的例子,可以說明什麼叫做“效能讓位於可維護性”:
- 如果為了效能,破壞了程式碼的可維護性,那麼我們寧願不要效能;
- 在能夠保證可維護性的前提下,我們當然應該努力的提高效能;
- 較之於在區域性(非效能瓶頸處)糾結髮力,不如在架構的層面上保證/促進整體效能的提高。
我說提到的“效能的問題先不管”,以及“忘記資料庫”等,是基於矯枉必須過正的出發點,希望能夠有振聾發聵的效果。但結果看來不是很好,評論裡我還是看到了“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是一個開銷很大的過程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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(); } |
其次,在BaseService中暴露一個靜態的EndSession()方法,在Request結束時將資料的變化同步到持久層(資料庫)。所以當UI層呼叫時,不需要例項化一個BaseService,只需要BaseService直接呼叫即可:
1 2 3 4 5 6 7 |
public class BaseService { public static void EndSession() { } } |
然後,我們回頭看看前面的說法:“一旦HTTP request到達,就生成一個session;”,所以理論上需要一個InitSession()的方法,生成/提供一個session。但我突然有了點小聰明:有些頁面可能是不需要資料庫操作的,比如幫助、表單呈現,或者其他我們暫時想不到的頁面。那我們無論如何總是生成一個session,是不是浪費了點?
越想越覺得是這麼一回事,所以左思右想,弄出了一個方案:按需生成session。大致的流程是:
- 嘗試獲取session;
- 如果“當前環境”中已有一個session,就直接使用該session;
- 否則就生成一個session,使用該session,並將其存入當前環境中。
看來NHibernate支援這種思路,所以提供了現成的介面,可以很方便的實現上述思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
protected ISession session { get { ISession _session; if (!CurrentSessionContext.HasBind(sessionFactory)) { _session = sessionFactory.OpenSession(); CurrentSessionContext.Bind(_session); } else { _session = sessionFactory.GetCurrentSession(); } return _session; } } |
其中CurrentSessionContext就是上文所謂的“當前環境”,在我們的系統中國就是一個HttpContext;我們使用GetCurrentSession()就總是能夠保證取出的session是當前HttpContext中已有的session。所有的Service都繼承自BaseService,直接呼叫BaseService中的session,這樣就可以有效的保證了Session Per Request的實現。
同學們,這下知道了吧?其實我骨子裡還是一個很“摳”效能的人。但這樣做究竟值不值?我也不太確定,畢竟這樣做一定程度上增加了程式碼的複雜性,而所獲得的效能提升其實有限。
總是使用顯性事務
如果同學們檢視原始碼,就會發現,我們的session總是啟用了事務。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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生成情景。
程式碼非常簡單,如下:
1 2 3 4 5 6 7 8 9 10 11 |
public class SessionPerRequest : ActionFilterAttribute { public override void OnResultExecuted(ResultExecutedContext filterContext) { #if PROD FFLTask.SRV.ProdService.BaseService.EndSession(); #endif base.OnResultExecuted(filterContext); } } |
#if PROD的使用是為了前後端分離(後文詳述):只有當呼叫ProdService時才使用以上程式碼,UI開發人員使用UIDevService時不需要改項操作。
同時,為了避免反覆的宣告,我們提取出BaseController,由所有Controller繼承,並在BaseController上宣告SessionPerRequest即可:
1 2 3 4 |
[SessionPerRequest] public class BaseController : Controller { } |
其他
由於我們在Action呈現後實現資料的同步(session.Transaction.Commit()),所以我們所有的Ajax呼叫,沒有使用Web API,而是繼承自ActionResult的JsonResult。否則,不會觸發OnResultExecuted事件,也無法同步資料庫。
1 2 3 4 5 |
public JsonResult GetTask(int taskId) { string title = _taskService.GetTitle(taskId); return Json(new { Title = title }); } |
綜上,我們實際上是借鑑了SessionPerRequest的思路,實際上採用了按需生成Session、且一個Action使用一個session的實現。可以描述成:SessionPerActionIfRequire,呵呵。
通過SessionPerRequest,我們可以發現架構的一個重要作用:將系統中“技術複雜”的部分封裝起來,讓開發人員可以脫離複雜瑣碎的技術,而專注於具體業務的實現。事實上,採用我們的系統,即使一個不怎麼懂NHibernate的普通開發人員,經過簡單的介紹/培訓,也可以迅速的開始業務領域程式碼的編寫工作。