天天看點

頁面片“.NET技術”段緩存(一)

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

  而且很多系統的頁面左上角都有一個該死的“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

由于一個頁面可能有很多片段,不同的片段肯定要用不同的cache key,是以這個标簽應該還能傳入一個cache key。當呈現這個頁面,到解析這個标簽的時候我們就用這個cache key去緩存中取,如果取到了我們就直接将緩存的東西輸出,

  而不再需要解析這個圖書清單了。

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

public class Cache extends Directive {

@Override

public String getName() {

return "cache";

}

public int getType() {

return BLOCK;

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)

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

private DataProvider dataProvider;

public BookController(BookDataProvider dataProvider) {

this.dataProvider = dataProvider;

return new ModelAndView("list", 上海企業網站制作yle="color: #800000;">"dataProvider", dataProvider);

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

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);

  我們在标簽内部取到外部傳入的dataProvider,它實作了一個接口DataProvider,然後在标簽内部進行資料的查詢。

  然後将查詢的資料put到velocity的context中,然後再次render,将render的結果放到緩存。這下好了,控制器裡隻需要向視圖傳遞一個可以取資料的對象就可以了,cache标簽内部會進行判斷。DataProvider和BookDataProvider的代碼:

public interface DataProvider {

Map<String,Object> load();

@Service

public class BooksDataProvider implements DataProvider{

public BooksDataProvider(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自定義标簽的使用

  每個自定義标簽都從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)

...

那麼在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來做片段緩存。

繼續閱讀