上一篇下一篇的优化过程

作者:  最后修改:2014年12月25日  浏览数:408
一、使用方式
有些项目需要实现在内容详细页中使用上一篇下一篇功能,使用方式类似于:
<div>
  上一篇: <a href="${Article.PrevLink}">${Article.PrevTitle}</a><br>
  上一篇 <a href="${Article.NextLink}">${Article.NextTitle}</a><br>
</div>
除此之外还有${Article.PrevLogoFile}和${Article.NextLogoFile}用来显示上一篇下一篇的引导图。
 
 
二、性能代价太大
OrderFlag是内容的排序标识,默认为添加时间的毫秒数乘以100,置顶时再乘以100以保证排在最前面,取消置顶除以100使其返回原来的位置。 因此上一篇下一篇的功能可以很简单地实现:在发布前去数据库中根据OrderFlag查询上一篇下一篇的记录,然后根据查询结果添加相应的模板变量即可,代码参见PrevAndNextBL.getNextContent()/getPrevContent()方法。 但这种实现方式有一个很大的缺陷:性能太差,每发布一篇内容都要多两次数据库查询。以发布栏目下的详细页为例,每分钟发布数只有以前的50%不到,性能损失非常严重。并且有的项目的模板中并没有引用上一篇下一篇相关的变量,但也要查询两次。为了这样一个小功能付出这么大的代价很显然不值,因此必须优化。 
 
 
三、setKeyNotFoundEventListener的引入
Mapx有一个setKeyNotFoundEventListener方法,可以在Mapx找不到键值时用一个KeyNotFoundEventListener去加载值。通过这种方式可以实现在模板中引用了上一篇下一篇有关的模板变量时再去数据库中查询,从而能够优化某些情况下的性能。相应的代码调用路径:
AbstractDetailTemplate.getContext(IContent content, String platformID, boolean preview)
AbstractDetailTemplate.getContext.addOtherVariables(final IContent content, final AbstractExecuteContext context)
PrevAndNextBL.addTemplateVariables(final AbstractExecuteContext context, final IContent content, final Mapx<String, Object> map)
 
 
四、保存时计算
但setKeyNotFoundEventListener只是在未使用上一篇下一篇功能时免去不必要的性能损失,但在确实使用了此功能时无能为力,因此还需要进一步优化。考虑到内容发布和内容保存时的不对称性(极少有网站每分钟新建或修改的文章能够超过5篇,所以内容保存时慢100毫秒都问题不大; 但发布时往往有一次发布上十万篇内容的情况,每一篇只慢10毫秒,则累计会慢16分钟),所以可以改为在内容保存时查询上一篇和下一篇的ID并保存在扩展属性中,然后发布时只根据保存的ID去取内容本身。因为ID是主键,因此这种做法会比按OrderFlag大于小于关系去查询快很多。 经测试,直接按OrderFlag查询每分钟发布数量会减少一半,但按ID去查,每分钟发布数量只会减少20%左右。相应的代码调用路径: 
ContentEditorUI().save()
PrevAndNextBL.computePrevAndNextID(ZCContent c)
 
 
五、栏目整体发布时优化
在栏目整体发布时上一篇下一篇的数据实际上已经取出放在内存中了,没有必要再根据扩展属性中的ID去查询。相应的代码调用路径:
PublishBL.publishCatalogContents(ZCCatalog catalog, int status, LongTimeTask task)
PrevAndNextBL.dealContentList(List<IContent> list, int status, int pageIndex)
在dealContentList()中会将上一篇下一篇的IContent实例放到当前IContent的props中,然后在addTemplateVariables时会检查props中有没有相应的对象,如果有就直接使用,没有就根据ID查询。 进行上述调整后,每分钟发布数量只减少了5%,性能损失已经可以接受了。 
 
 
六、考虑导入的内容
通过ZCMS录入的内容保存时都会调用ContentEditorUI().save(),因此都会在扩展属性中保存上一篇下一篇的ID,但导入的内容可能不会有这个扩展 属性的值,因此如果没有值时还是要实时查询数据库,相应的逻辑在 
PrevAndNextBL.onVariableNotFound(AbstractExecuteContext context, IContent content, Mapx<String, Object> map)中
 
 
七、考虑特殊情况
通过以上调整,性能优化的目的达到了,但在一些特殊情况下会造成上一篇和下一篇不正确的问题,主要有:
1、置顶和取消置顶
2、删除和还原
3、移动和排序
5、一次发布多篇文章(因为多篇文章在发布前都是未发布状态,所以查询上一篇下一篇可能会不正确)
这些情况都涉及到位置的变化,要处理原来位置上下篇的扩展属性和新位置的上下篇的扩展属性,又因为可能一次处理多个内容,因此非常复杂。 如果逐一修改各个地方的逻辑,则工作量太大,考虑到这些操作都会引起栏目列表页的重新发布,因此采取了一种迂回的方式: 
在栏目列表页中都会调用内容列表标签,各个内容类型都不一样但都继承自AbstractDetailTemplate,因此可以在AbstractDetailTemplate中增加 以下检查逻辑:如果当前模板是栏目列表页模析则检查标签准备好的DataTable中的数据,如果DataTable中的ConfigProps列中的上一篇下一篇的值和DataTable自身的顺序不一致,则修改相应的ZCContent记录并重新发布对应的内容。
通过这种方式可以保证各种特殊情况下的内容详细页中的上一篇下一篇和栏目列表中的顺序保持一致。