【编者按】本文作者自由飞,具有传奇般的人生经历
- 98年读大学-国际贸易专业
- 03年11月英语培训机构当英语老师
- 04年2月-05年6月律师事务所实习和公司法务
- 05年6月-07年12月成立装饰公司做老板
- 08年8月开始自学编程
- ……
伤感于《野生程序言的故事》一文评论中同学们普遍性的自怨自艾,回顾自己求学探索的艰辛,愿意做一些力所能及的事,帮助所有立志于自学和成长的同学。本《架构之路》系列,是他以两个目前仍在开发的项目为例,讲解如何通过领域驱动和测试驱动,进行敏捷开发,构建一个面向对象的B/S系统的一次尝试。
同时,欢迎有兴趣的同学参与(详见:英雄帖:开源项目招募英才)。
本文为他倾囊相授的第九篇:
- 第八篇:《架构之路(八):从CurrentUser说起 》;
- 第七篇:《架构之路(七):MVC点滴》;
- 第六篇:《架构之路(六):把框架拉出来 》;
- 第五篇:《架构之路(五):忘记数据库》;
- 第四篇:《架构之路(四):测试驱动》;
- 第三篇:《架构之路(三):单元测试》;
- 第二篇:《架构之路(二):性能》;
- 第一篇: 《架构之路(一):目标》。
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程序的标配。
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直接调用即可:
EndSession:
public class BaseService
{
public static void EndSession()
{
}
}
然后,我们回头看看前面的说法:“一旦HTTP request到达,就生成一个session;”,所以理论上需要一个InitSession()的方法,生成/提供一个session。但我突然有了点小聪明:有些页面可能是不需要数据库操作的,比如帮助、表单呈现,或者其他我们暂时想不到的页面。那我们无论如何总是生成一个session,是不是浪费了点?
越想越觉得是这么一回事,所以左思右想,弄出了一个方案:按需生成session。大致的流程是:
- 尝试获取session;
- 如果“当前环境”中已有一个session,就直接使用该session;
- 否则就生成一个session,使用该session,并将其存入当前环境中。
看来NHibernate支持这种思路,所以提供了现成的接口,可以很方便的实现上述思路:
按需获取session:
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生成情景。
代码非常简单,如下:
调用EndSession():
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声明:
[SessionPerRequest]
public class BaseController : Controller
{
}
其他
由于我们在Action呈现后实现数据的同步(session.Transaction.Commit()),所以我们所有的Ajax调用,没有使用Web API,而是继承自ActionResult的JsonResult。否则,不会触发OnResultExecuted事件,也无法同步数据库。
AJAX返回JsonResult:
public JsonResult GetTask(int taskId)
{
string title = _taskService.GetTitle(taskId);
return Json(new { Title = title });
}
综上,我们实际上是借鉴了SessionPerRequest的思路,实际上采用了按需生成Session、且一个Action使用一个session的实现。可以描述成:SessionPerActionIfRequire。
通过SessionPerRequest,我们可以发现架构的一个重要作用:将系统中“技术复杂”的部分封装起来,让开发人员可以脱离复杂琐碎的技术,而专注于具体业务的实现。事实上,采用我们的系统,即使一个不怎么懂NHibernate的普通开发人员,经过简单的介绍/培训,也可以迅速的开始业务领域代码的编写工作。
(责编/钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,交流探讨可加微信qshuguang2008,备注姓名+公司+职位)
「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008入群,备注姓名+公司+职位。