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

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直接調用即可:

public class BaseService
{
public static void 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;
}
}
其中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);
}
}
#if PROD的使用是為了前后端分離(后文詳述):只有當調用ProdService時才使用以上代碼,UI開發人員使用UIDevService時不需要改項操作。
同時,為了避免反復的聲明,我們提取出BaseController,由所有Controller繼承,并在BaseController上聲明SessionPerRequest即可:

[SessionPerRequest]
public class BaseController : Controller
{
}
其他
由于我們在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 });
}
綜上,我們實際上是借鑒了SessionPerRequest的思路,實際上采用了按需生成Session、且一個Action使用一個session的實現。可以描述成:SessionPerActionIfRequire,呵呵。
通過SessionPerRequest,我們可以發現架構的一個重要作用:將系統中“技術復雜”的部分封裝起來,讓開發人員可以脫離復雜瑣碎的技術,而專注于具體業務的實現。事實上,采用我們的系統,即使一個不怎么懂NHibernate的普通開發人員,經過簡單的介紹/培訓,也可以迅速的開始業務領域代碼的編寫工作。
+++++++++++++++++++++++++++++
應該是2016年春節前最后一篇《架構之路》的更新了,先預祝大家新春快樂,萬事如意!
另外,歡迎各種留言評論(包括拍磚)。
O(∩_∩)O~
文章列表