片段缓存的实际应用、延迟加载及Eazy类库
2009-09-22 14:54 by 老赵, 14170 visits片段缓存(二,三)已经实现完整了,但好像还没有提到如何在项目中进行实际应用,那么现在就来谈一谈这方面。之前也有朋友提出,这个片段缓存到底省下的是什么啊?好像数据都是在Controller中获取的,视图的生成不会带来多少开销啊,难道节省的只是拼接HTML字符串的时间吗?这其实就涉及到片段缓存在实际项目中该如何使用的问题了。
上周日的幻灯片中我提到Ruby on Rails中的Fragment Caching一般是这样使用的:
class BlogController < ApplicationController def list unless read_fragment(:action => 'list' ) @articles = Article.find_recent end end end
以及它的视图模板:
<% cache do %> <!- Here's the content we cache -> <ul> <% for article in @articles -%> <li><p><%= h(article.body) %></p> <% end -%> </li></ul> <% end %>
以上是一个Controller和一个名为list的Action方法,再list方法中回去检查片段缓存是否存在,如果不存在才去加载@articles字段。视图模板中的逻辑也类似,如果缓存不存在才去访问@article字段。这样,读取@acticle的开销便节省下来了。在我之前公布的ASP.NET MVC片段缓存实现中,还无法在Controller中操作缓存,近期打算加上这方面的功能。
可是这样的做法可能会产生一个问题:在并发情况高的环境下,可能视图会访问到没有初始化的@articles字段。因为在Action方法中缓存还没有过期,但是就在正式生成视图内容,缓存过期了——于是就引起了错误。不过,其实我在想到“片段缓存”时,第一反应并不是这种做法,而是使用“延迟加载”。
使用延迟加载,也就是说在Action方法中虽然不会加载某个字段,但是还是会给这个字段“打一个桩(stub)”。如果视图中不妨问这个字段自然无妨,但在需要访问的话,也可以正常获取到数据。于是这种做法既可以省下开销,又不会出现问题。
不过使用延迟加载并非完全没有代价,它要求资源回收的时机不能太早。据我所知,有些朋友会使用Action Filter,在OnActionExecuted时回收所有资源(如数据库连接),这样在视图中自然无法获取数据了。因此,如果您使用延迟加载,请务必在OnResultExecuted时回收资源。
那么,我们该如何使用延迟加载呢?最容易的做法自然是稍微修改一下你的Model,保持接口不变即可。不过现在您也可以关注一下Eazy类库。
Eazy类库是我前一段时间设想中的“延迟辅助类库”,名字来源于Easy + Lazy,目前托管在CodePlex中。有了这个类库的帮助,您就无需对自己的类库“小动干戈”了。于是,您就可以在Action方法使用这样的代码来设置字段的延迟功能:
public ActionResult List() { var model = LazyBuilder.Create<Model>() .Setup(m => m.Articles, () => Article.FindRecent()) .Instance; return View(model); }
LazyBuilder.Create方法会创建Model类型的实例,然后使用Setup方法可以为某个属性指定一个委托,而这个委托便会在第一次访问这个属性时执行。LazyBuilder的实现机制非常清晰,只是使用Emit在运行时动态创建目标类型的子类而已。例如Model类型:
public class Model { public virtual List<Article> Articles { get; set; } }
便会为它生成如下的子类:
public class Model$LazyProxy : Model { public override List<Article> Articles { get { if (this.Articles$LazyLoader != null) { base.Articles = this.Articles$LazyLoader(); this.Articles$LazyLoader = null; } return base.Articles; } set { base.Articles = value; this.Articles$LazyLoader = null; } } public Func<List<Article>> Articles$LazyLoader = null; }
这段写法是我认为实现延迟加载最理想的方式了,最重要的是它保留了属性原有的逻辑,只是在加载时机上做了文章。此外,在不需要延迟加载的情况下,属性的行为也不会改变。
不过Eazy项目其实才创建了2天,目前只能通过指定泛型类型来构造对象,这意味着这个类型必须有默认的构造函数。根据我的设想,它以后还会支持其他的构造方式,例如:
var builder = LazyBuilder.Create(() => new Model(1, 2) { Time = DateTime.Now }); builder.Setup(m => m.Articles, () => Article.FindRecent()); var model = builder.Instance;
由于完全使用Emit,因此不会带来反射的开销,性能是很有保障的。例如LazyBuilder.Create<Model>的消耗和new Model()相比也就是多了些方法调用而已。目前Eazy项目还比较简陋,例如代码中不会抛出恰当的异常,单元测试也不够完整。此外,我还在考虑是否要对只读属性或接口提供延迟加载的支持。
当然,如果您有什么想法也请告诉我,这里先谢过了。
昨天关心的问题,今天有了全面方案了。呵呵。