系统性能提升之道--内存镜像表
提出问题
对于一个系统,我们在设计开发时,不得不考虑系统的性能问题,硬件的提速可以缓减系统日益增长的消耗,但我们也不能肆无忌惮的扩展系统而不考虑性能的提高,我们应该重视资源的有限性。
为了说明问题,我先举个例子,有两个表如下:
Items物料表
字段名 |
数据类型 |
描述 |
ID |
Varchar(50) |
主键(PK) |
Name |
Varchar(50) |
物料名称 |
CatalogId |
Varchar(50) |
物料组ID(FK,关联Catalog表) |
…… |
…. |
….. |
Catalog物料组表
字段名 |
数据类型 |
描述 |
ID |
Varchar(50) |
主键(PK) |
Name |
Varchar(50) |
物料名称 |
物料表的CatalogId关联物料组表的主键
要求:我们在显示物料信息时,显示此物料的物料组名称
传统的非ORM模式下,我们一般可以采用
的语句来读取,然后可以在Grid上直接绑定”catalogName”来显示物料组名。这种非ORM模式开发的系统,系统扩展性非常差、数据库移植性差、开发效率低下,所以大家也都开始使用ORM的开发模式。此文章不推荐、不讲述非ORM的开发。
ORM模式下,我们把这两个表映射为“ItemsEntity物料实体类”与“CatalogEntity物料组实体类”。然后我们在Retrieve到一个物料对象aItem后,在显示时要求显示物料组名的话,就需要CatalogEnity去Retrieveg一个物料组对象aCatalog,使用此aCatalog的Name属性才能显示出来。
我们也发现了,在ORM模式下,由于采用了OO的编程模式,对开发者来说带来了很多的方便,但在过程中,进行了两次数据库访问,我们试想一下,要是此Items物料表有十个类型的外键关联,那么一个Items的显示,将进行十一次数据访问,对于极其宝贵的数据库访问资料来说,性能大打折扣了。
NHibernate中的解决方案:
在NHibernate中,可以采用“Relation”的方式来解决,需要在XML中配置十个one-to-many的“Relation”,然后采用非Lazy的方式进行一次性读取,这种方式是把十一次的数据访问放在一起执行了,这看起来可以很方便的实现;但如果有N个人都要进行这样的操作,那么数据访问数还是非常大(N*11)。而且我非常不喜欢NH里的“Relation”,因为这“Relation”还存在一个问题,返回的是对象数组,在没有Grid控件绑定的Java中,这可能是不错的方式,可是在.NET中,我们更会选择Grid进行DataTable的数据绑定,因此这种“Relation”无法很方便的返回DataTable,所以从目前来看,这种“Relation”在.NET中非常不适用的。这也是我的SPL是没有添加“Relation”的原因之一。
缓存内存表的概念:
对于上面的这种情况,我们可以思考一下,采用另一种更好的方式解决。我们可以把这个物料组表的内容放到内存里,作为一个“内存镜像表”,此“内存镜像表”是数据库中实际表的一个镜像,实时保持与实际表的同步,我们在读取时:
if(内存镜像存在)
从内存直接读取;
else
{
从数据库读取;
建立内存镜像表,把数据库中的数据镜像到内存表中;
}
这样的方式可以减少很多的数据访问,就拿上面的例子中,在第一个人进行了十一次访问后,以后的N个人执行,只要执行N次数据访问了,速度将会有一个突破性的提高。
内存镜像表的可行性分析:
对于这种“内存镜像表”的可行性我们先从数据库数据来分析。
一个系统将包括很多的数据,但我们可以对这些数据进行分一下类:
“维护性数据”:
这是指那些在系统中的基本维护性数据,比如前面的物料组、客户类型、付款方式、结算方式、销售类型、货币种类等等,大家都可以举出很多很多这种类似的数据,这些数据存在的共同点是:
数据量小:这种数据一般在50条记录以内,都是一些类型,分类之类的数据
字段少:这种数据的字段都是很少的,主要是ID、名称、状态等
使用频繁高:这种数据的使用率非常频繁,因为在其他表中作为外键,经常会被使用
维护少:这种数据一般在系统启用前期进行维护,大部分情况下不维护
因此,具有这些特点的数据,我们就非常有理由放到内存中进行缓存起来。
“操作型数据”:
这种数据的特点是数据量比较大,字段也比较多,比如物料、客户等信息,不适合用到内存中。
“日志型数据”:
这种数据数据量就更多的,比如订单、订单明细、操作记录等,这就更不合适了。
因此我们主要是考虑“维护性数据”,大家想想在你的系统中,会有多少是这种“维护性数据”,对于结构相似的“维护性数据”都会采用“数据字典”的方式来对待。
内存镜像表的可操作性分析:
在可行性分析后,我们要考虑,在实际操作起来要注意哪些。
大家也很容易发现,唯一注意点就是“如何保持实时的镜像关系”,也就是要实时保持内存中镜像与实际的数据库表数据一致。
那么我们就要保证在进行更新(包括新增,更新,删除)时能同时更新内存表数据就可以了。
对于非ORM的开发模式来说,使用Sql语句进行数据访问的,要实现这种“实时同步”是很难的,幸好的是我们现在有了ORM的开发模式,在ORM上,我们的数据访问不是直接使用SQL的,而是通过中间的持久层(PersistenceLayer)来实现的,因此,这个持久层(PersistenceLayer)为实现这种同步提供了必不可少的条件。
在OR Mapping时,我们标识一个实体为“需保存到内存镜像”,那么持久层(PersistenceLayer)在进行此对象的Save()、Update()、Delete()时,自动进行“内存同步处理”即可,而且这是完全可实现的。
好了,我们回到前面的例子中,那样的一个物料Retrieve()操作,其实只要两次访问数据库,而且当有N个用户进行类似的操作时,由于“内存镜像表”在第一次就载入到内存了,因此访问数据库次数为N次了。
内存镜像表的弊端性:
从上面的操作性我们就可以看出了,效率的提高是建立在内存的牺牲上的,如果有过多的“维护型数据”使用“内存镜像”,那么,整个内存消耗将非常大,所以,在考虑使用多少“内存镜像表”时,是要考虑服务器的承受能力的。
但我想,增加内存,提高性能这是很多客户所在追求的。因此,我们只要能仔细分析,是可以得出一种好的“内存镜像表”量的。
内存镜像表的实现性:
这种“内存镜像表”在我的SPL(SmartPersistenceLayer)已经实现了,不知道NH等其他的持久层中是否也有类似的功能。
在SPL中的使用:
在ClassMap中定义实体类的IsSaveToMemory为true即可。例:
<attribute name="Id" column="Id" type="Integer" increment="true" key="primary" />
<attribute name="No" column="No" type="String" key="primary" />
<attribute name="Name" column="Name" type="String" />
<attribute name="Birthday" column="Birthday" type="Date" />
<attribute name="Grade" column="Grade" type="Integer" />
<attribute name="Score" column="Score" type="Decimal" />
</class>
SPL的实现细节:
在SPL中有个静态类,它的ArrayList里存放各种“内存镜像表”的“镜像DataTable”,在进行Retrieve()时,先判断此ArrarList中是否存在此实体的“镜像”,如果有的话,则直接从此“镜像DataTable”中Retrieve()出来,还有RetrieveCriteira(获取标准)时,会从此“镜像DataTable”中使用Select符合条件的,生成新的“镜像DataTable”返回出来。
SPL的实体在进行Save()、Delete()和UpdateCriteria(更新标准)以及DeleteCriteria(删除标准)时都是会进行“镜像DataTable”的同步更新。
但这种也不能完全意义上的保证“同步”,因为SPL支持Sql语句执行,所以不要使用Sql来更新“内存镜像”的表。要不然,就无法保证“内存同步”了。
听棠