代码改变世界

片段缓存的实际应用、延迟加载及Eazy类库

2009-09-22 14:54 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

片段缓存)已经实现完整了,但好像还没有提到如何在项目中进行实际应用,那么现在就来谈一谈这方面。之前也有朋友提出,这个片段缓存到底省下的是什么啊?好像数据都是在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项目还比较简陋,例如代码中不会抛出恰当的异常,单元测试也不够完整。此外,我还在考虑是否要对只读属性或接口提供延迟加载的支持。

当然,如果您有什么想法也请告诉我,这里先谢过了。