頁面片段緩存(一)

作者: 橫刀天笑  來源: 博客園  發布時間: 2011-06-04 13:51  閱讀: 10428 次  推薦: 0   原文鏈接   [收藏]  

  一般,頁面上會分為很多部分,而不同的部分更新的頻率是不一樣的。如果對整個頁面采用統一的緩存策略則不太合適,

  而且很多系統的頁面左上角都有一個該死的“Welcome XXX”。這種特定于用戶的信息我們是不能緩存的。對于這些情況我們就需要使用片段緩存了。對頁面不同的部分(片段)施加不同的緩存策略,而要使用片段緩存,首先就得對頁面進行切分。土一點的辦法可以用iframe,用iframe將頁面劃分為一塊塊的,不過我總覺得iframe是個邪惡的東西。好點的辦法可以用Ajax單獨的請求這個片段的內容然后再填充,看起來挺美好的。不過使用Ajax也有一些限制:

  1、如果頁面上有許多片段,使用太多的這種技術,會有很多請求發送到服務器,HTTP對同一個域名有連接的限制,這樣會降低并發連接的效率。

  2、如果說第一個不是什么問題,那么還有一點可能對用戶體驗不友好。比如有一個片段可能響應慢點,造成頁面閃爍。不過如果前面兩點都可以克服,這個方案還是可以的。可惡的是我們的客戶(此處省略500字),說他們的大多數用戶處于一個禁用JavaScript的環境里。好吧,這個方案也不能使用了。如是我們進行了一系列其他關于片段緩存的嘗試:

  我們的系統使用的是Spring+Hibernate+Oracle技術,模板引擎使用的是Apache Velocity。假設下面的片段是我們要緩存的內容:

 
<ul>
#foreach($book in $books)
<li><a href="/book/books/$book.id">$book.name</a>---<a href="/book/books/edit/$book.id">Edit</a> -- <a href="/book/books/delete/$book.id">Delete</a></li>
#end
</ul>

  顯示一個圖書列表。對這個頁面改動最小的辦法是加上一個標簽,被這個標簽包圍的片段就是緩存的:

 
#cache
<ul>
#foreach($book in $books)
<li><a href="/book/books/$book.id">$book.name</a>---<a href="/book/books/edit/$book.id">Edit</a> -- <a href="/book/books/delete/$book.id">Delete</a></li>
#end
</ul>
#end
由于一個頁面可能有很多片段,不同的片段肯定要用不同的cache key,所以這個標簽應該還能傳入一個cache key。當呈現這個頁面,到解析這個標簽的時候我們就用這個cache key去緩存中取,如果取到了我們就直接將緩存的東西輸出,

  而不再需要解析這個圖書列表了。

  有了這個想法,我們就需要找到如何讓Velocity解析我們的標簽的方案。很好,Velocity是支持自定義標簽的:

 
public class Cache extends Directive {
@Override

public String getName() {
return "cache";
}

@Override

public int getType() {
return BLOCK;
}
@Override

public boolean render(InternalContextAdapter context, Writer writer, Node node)
throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {
Node keyNode
= node.jjtGetChild(0);

String cacheKey
= (String) keyNode.value(context);
String cacheHtml
= cacheHtml = (String) CacheManager.getInstance().get(cacheKey);
if (StringUtils.isEmpty(cacheHtml)) {
Node bodyNode
= node.jjtGetChild(1);
Writer tempWriter
= new StringWriter();
bodyNode.render(context, tempWriter);
cacheHtml
= tempWriter.toString();
CacheManager.getInstance().
set(cacheKey, cacheHtml);
}
writer.write(cacheHtml);

return true;
}
}

  關于Velocity的自定義標簽的使用我會在后面稍作解釋。

  最主要的邏輯在render方法里,我們先根據cache key去緩存里取,如果沒取到再使用代碼render,然后render的結果放到緩存中。很典型的緩存使用場景是不。再來看看控制器端得代碼:

 
@Controller
@RequestMapping(
"/books")
public class BookController {
private BookDAO bookDAO;
@Autowired

public BookController(BookDAO bookDAO) {
this.bookDAO = bookDAO;
}

@RequestMapping(value
= {"", "index.html"}, method = RequestMethod.GET)
public ModelAndView index() {
return new ModelAndView("list", "books", bookDAO.findAll());
}
}

  控制器很簡單,調用DAO,將所有圖書列出來即可。正在我們高興這么棘手的問題被解決的時候,問題來了:

  我們的緩存是為了什么?總不是為了節約Velocity解析的時間吧。我想大家應該都知道,最主要的還是為了節約這次bookDAO.findAll()查詢數據庫的時間。但是回過頭看看我們的方案。不管我們的cache命沒命中,這個bookDAO.findAll()都會執行一次,因為控制器的執行是在視圖render之前發生的。我們唯一節省的是Velocity解析的時間,杯具。

  找到了問題的答案,尋找解決辦法就容易了。我們要做的就是在緩存沒有命中的時候才執行查詢,那么這個數據查詢就必須放到cache標簽內部做。但是我們的cache標簽可不是為了一個片段啊,有很多片段,而各種片段取數據的方式卻不同。

  嗯,你還記得接口么?還記得計算機里所有的問題都可以通過中間層解決的這個名言么?按照這個思路我們如此設計cache標簽:

 
#cache("book_list",$dataProvider)
<ul>
#foreach($book in $books)
<li><a href="/book/books/$book.id">$book.name</a>---<a href="/book/books/edit/$book.id">Edit</a> -- <a href="/book/books/delete/$book.id">Delete</a></li>
#end
</ul>
#end

  我們傳入一個dataProvider對象進來,而這個dataProvider是控制器里傳入進來的,一個專門用來取數據的:

 
public class BookController {

private DataProvider dataProvider;
@Autowired

public BookController(BookDataProvider dataProvider) {
this.dataProvider = dataProvider;
}

@RequestMapping(value
= {"", "index.html"}, method = RequestMethod.GET)
public ModelAndView index() {
return new ModelAndView("list", "dataProvider", dataProvider);
}
}

  控制器還是一如既往的簡單,我們再來看看cache標簽的實現:

 
public class Cache extends Directive {
@Override

public String getName() {
return "cache";
}

@Override

public int getType() {
return BLOCK;
}

@Override

public boolean render(InternalContextAdapter context, Writer writer, Node node)
throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {
Node keyNode
= node.jjtGetChild(0);

String cacheKey
= (String) keyNode.value(context);
String cacheHtml
= cacheHtml = (String) CacheManager.getInstance().get(cacheKey);
if (StringUtils.isEmpty(cacheHtml)) {
Node dataProviderNode
= node.jjtGetChild(1);
DataProvider dataProvider
= (DataProvider) dataProviderNode.value(context);
Map
<String, Object> map = dataProvider.load();
for (String key : map.keySet()) {
context.put(key, map.
get(key));
}

Node bodyNode
= node.jjtGetChild(3);
Writer tempWriter
= new StringWriter();
bodyNode.render(context, tempWriter);
cacheHtml
= tempWriter.toString();
CacheManager.getInstance().
set(cacheKey, cacheHtml);
}
writer.write(cacheHtml);

return true;
}
}

  我們在標簽內部取到外部傳入的dataProvider,它實現了一個接口DataProvider,然后在標簽內部進行數據的查詢。

  然后將查詢的數據put到velocity的context中,然后再次render,將render的結果放到緩存。這下好了,控制器里只需要向視圖傳遞一個可以取數據的對象就可以了,cache標簽內部會進行判斷。DataProvider和BookDataProvider的代碼:

 
public interface DataProvider {
Map
<String,Object> load();
}
@Service

public class BooksDataProvider implements DataProvider{
private BookDAO bookDAO;

@Autowired

public BooksDataProvider(BookDAO bookDAO){
this.bookDAO = bookDAO;
}

public Map<String, Object> load() {
Map
<String,Object> result = new HashMap<String,Object>();
result.put(
"books",bookDAO.findAll());
return result;
}
}

  現在我們要改造的就是對于每個不同的緩存片段寫一個DataProvider的實現,而實際上這個實現原來已經有了:

  就是原來控制器內那部分代碼。比如BooksDataProvider實際上就是原來BookController內的代碼。通過這種方式,我們基本上就將一個頁面劃分為很多用cache包圍的小片段了,只要劃分出來了那么你就可以對不同

  的片段采用不同的緩存策略。代碼的結構還算清晰。

  下面我稍微介紹一下Velocity自定義標簽的使用

  Velocity自定義標簽

  每個自定義標簽都從Directive派生下來,我們要覆蓋幾個方法。

  1、getName,返回一個字符串,這個就是你在velocity模板里使用的那個標簽的名字:#cache。

  2、標簽的類型,像我們的cache這種#cache…#end的叫塊級標簽,那么返回的就是一個BLOCK(常量1)。還有一個類型是LINE(2),那就沒有那個#end了。

  3、render方法,這是最主要的,你可以覆蓋一些行為。

  取外部傳入的參數

  那么在標簽內部如何取得外部傳入的參數呢?其實它的行為和xml path的操作方式差不多。在render方法的參數里有一個Node,這個就是標簽自身。我們可以通過node.jjtGetChild(index)來取得各種參數。以0開始,如果是BLOCK類型的標簽,那么最后一個就是標簽包圍的內容了(比如我們的cache標簽)。然后我們可以通過node的value取到參數的值。取值的時候還將context傳入進去了,這說明這個值是可計算的。比如現在有這么一個需求,我們的圖書列表是分頁的,但是只緩存第一頁,后面的不緩存。那我們就期望能傳入

  一個表達式,讓cache標簽自己計算一把,如果這個表達式為true則緩存,否則不緩存:

 
#cache("book_list",$pageIndex==1,$dataProvider)
...
#end
那么在cache標簽內部呢:
 
Node needCacheNode = node.jjtGetChild(1);
Boolean needCache
= (Boolean) needCacheNode.value(context);

String cacheHtml
= StringUtils.EMPTY;
if (needCache) {
cacheHtml
= (tring) CacheManager.getInstance().get(cacheKey);
}

  這樣就可以計算出$pageIndex==1這個表達式的結果(當然,$pageIndex這個變量是需要傳入進來的)。

  好了,編寫好自定義標簽的代碼我們可以使用了。而并不是我們寫好這代碼往那兒一丟就可以使用了,還需要一個配置環節:

 
<bean class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
<property name="resourceLoaderPath" value="/WEB-INF/templates/"/>
<property name="velocityProperties">
<props>
<prop key="userdirective">com.yuyijq.web.Cache</prop>
</props>
</property>
</bean>

  好了,這么一個利用Velocity的片段緩存就完成了。但是這種方式也存在一些問題,給我們帶來了一些bug。

  1、有的時候我們會在片段里set一些變量,然后在這個片段外使用。但是現在使用了cache之后,cache之后的是HTML

  文本,這些變量全部消失了,那么片段外也取不到這些變量的值了。那我們就需要仔細搜查Velocity模板,將這些變量的使用全部移動到片段內部。如果是一開始就設計了這個片段緩存還好,我們可以注意這個問題。但問題是現在是項目的中途提出的,系統中velocity模板成千上萬,我們每緩存一個片段就要仔細檢查一番,而且Velocity模板沒有測試(當然可以寫測試,但很麻煩)。這個過程全部靠人肉,所以出bug的幾率會很高。

  2、還是一樣,改動比較大,不僅模板,后面的控制器也需要修改。不過貌似也沒什么更好的方法,要使用片段緩存貌似改動是避免不了的。

  成熟的方案

  片段緩存應該是大型系統里經常采用的,那么就應該有一些成熟的方案。我們為何不尋找那些成熟的方案要重新制造輪子呢。這個方案就是ESI,在下一篇文章中我會介紹結合Varnish和ESI來做片段緩存。

  頁面片段緩存(二)

0
0
 
標簽:ASP.NET 緩存
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()