正常設計思路
對這個需求,我們已經有sizeofengine用于計算記憶體中以及磁盤中的執行個體大小了,按我的正常思路,實作起來會比較簡單,直接在store中添加maxbytes以及currentbytes字段,用于表達目前store可以存放的最大執行個體大小以及目前store已存放的執行個體總大小。對memorystore,在添加新element之前使用defaultsizeofengine計算要添加執行個體占用記憶體的大小,然後判斷要添加的資料大小和currentbytes相加的值是否會超過maxbytes,如果是,先找出已經expired的element,将這些element移除,如果此時還沒能在maxbytes大小的限制中添加新的element,則對目前store做evict操作,evict出至少超過maxbytes大小的的資料大小;如果是更新原來已存在的element,一種方法先查找出已存在的element,然後計算新添加的element和原來element占用記憶體大小的delta值,如果該值大于0并且和currentbytes相加會大于maxbytes值,則如上做expire和evict操作,另一種方法是先移除已存在的element,然後對目前element做新添加element操作。對diskstore,使用disksizeofengine計算要添加的執行個體大小,其他和memorystore類似。
在ehcache中,memorystore和diskstore都是使用了segment的設計思想(不知道是否因為concurrenthashmap的影響,甚至guava cache也采用了這種segment的設計思想),因而我們可以将maxbytes、currentbytes抽象出一個maxbytesguarder類,并且将該執行個體指派給每個segment執行個體,作為面向對象設計的基本思路,将資料和操作放在一起它已經有了maxbytes和currentbytes資料了,我們就可以嘗試這将和它相關的操作添加到這個類中,比如執行個體占用記憶體、磁盤大小的計算,因而可以将sizeofengine引人該類中,另外它還可以包含一個elementevictor執行個體,它可用于evict操作,同時因為maxbytesguarder在每個segment共享,因而它必須是線程安全的。對更新已存在的element可以采用添加之前先移除的方法,這樣maxbytesguarder就可以設計成包含如下方法:
public interface maxbytesguarder {
public int getcurrentsize();
public void setmaxsize(int maxsize);
public int getmaxsize();
public int addwithevict(element element);
public boolean canaddwithoutevict(element element);
public void delete(int size);
public elementevictor getevictor();
public store getstore();
}
正常的cache設計思路是用memorystore存儲記憶體中的element,而将diskstore作為memorystore的從屬,隻有在evict發生才将element通過diskstore寫入磁盤,讀取時,隻有在memorystore中找不到對應的element時,才從diskstore中查找,并且将找到的element讀回memorystore,對這種設計maxbytesguarder隻需要在各自的store中存在即可。但是在ehcache目前store的設計中,diskstore并沒有作為memorystore的從屬,它們是單獨可用的store,ehcache使用diskbackedmemorystore把它們聯系起來,這個在接下來的文章中會詳細分析。在diskbackedmemorystore的實作中,diskstore作為authority,對每次新添加element,會首先添加到diskstore中,然後才是到memorystore,進而對memorystore來說,它在做evict時,直接删除即可,因為diskstore已經存儲了所有的element資訊。既然diskstore可以存儲記憶體中的element以及磁盤中的element,并且對磁盤中的element,ehcache也會在記憶體中有一些紀錄資訊,因而它需要同時控制記憶體中的element占用記憶體最大值以及磁盤中能保留的element的最大值。對這個需求,以上的maxbytesguarder就很難辦到了,因而ehcache做了進一步的抽象,把maxbytesguarder拆分成兩個接口:pool和poolaccessor,其中pool用于對maxsize的抽象,它包含poolevictor引用,已處理需要evict時的操作,通過pool建立poolaccessor,poolaccessor和一個store相關聯,用于處理和某個store相關的put、remove等操作,一個pool目前size是通過其建立的所有poolaccessor的size總和。
pool和poolaccessor設計
pool和poolaccessor的類結構圖如下:
從類結構圖中可以知道,pool包含maxsize、poolevictor、sizeofengine以及poolaccessor的list,并且pool内部所有poolaccessor的size和是目前pool執行個體目前使用的size。pool還可用于建立相應的poolaccessor執行個體,通過pool建立的poolaccessor會被自動的添加到pool中。在cache執行個體初始化時,它會根據配置建立相應的onheappool和ondiskpool,它們可以是unboundedpool,它的createpoolaccessor方法建立unboundedpoolaccessor執行個體;可以是boundedpool,用于建立atomicpoolaccessor;可以是strictlyboundedpool,用于建立lockedpoolaccessor。其中atomicpoolaccessor和lockedpoolaccessor的差別在于對size的鎖機制,atomicpoolaccessor使用atomiclong紀錄size的值而不是通過鎖,這樣會引起maxsize的限制有保證的情況,比如兩個線程同時在添加element,但是在一個線程還沒添加完成時另一個線程已經執行了判斷語句說目前還有足夠多的空間,然而事實上在第一個線程執行完成後,目前的空間可能已經不夠了,這種方式的好處是沒有鎖,效率很高,在那些對maxsize不是很敏感的項目中可以使用(感覺一般對這個值都不會很敏感);而lockedpoolaccessor則是采用鎖的機制來保證size值的一緻性,它一般用于那些對maxsize值特别敏感的項目中。poolaccessor由pool建立,除了從pool中繼承pool自身執行個體、sizeofengine執行個體以外,它還和一個poolablestore相綁定,這個綁定的poolablestore可以通過poolaccessor向pool中消費一個element大小的空間(add方法)、判斷是否可以在沒有evict的情況下消費一個element大小的空間(canaddwithoutevicting方法)、移除指定大小的空間、以另一個element大小的空間替換已存在的另一個element空間大小(replace)等。這裡的add方法,當可以成功的向這個poolaccessor消費一個element大小的空間時,它傳回新添加的element大小,否則傳回-1,表示添加失敗;另外對add方法的container表示存放該element的執行個體,比如hashentry(隻用于計算大小,因而一般都用一個空的hashentry來替代),對ondiskpoolaccessor來說,它用于表示一個diskmarker。
poolevictor
poolablestore在調用poolaccessor的add方法用于消費pool中一個element大小的空間時,會調用poolevictor的freespace将該pool相關聯的所有poolablestore釋放掉不夠的空間大小:
protected long add(long sizeof, boolean force) {
long newsize = getpool().getsize() + sizeof;
if (newsize <= getpool().getmaxsize()) {
// there is enough room => add & approve
size.addandget(sizeof);
return sizeof;
} else {
// check that the element isn't too big
if (!force && sizeof > getpool().getmaxsize()) {
// this is too big to fit in the pool
return -1;
}
// if there is not enough room => evict
long missingsize = newsize - getpool().getmaxsize();
if (getpool().getevictor().freespace(getpool().getpoolablestores(), missingsize) || force) {
size.addandget(sizeof);
return sizeof;
} else {
// cannot free enough bytes
}
}
poolevictor接口則是對如何從多個poolablestore中evict出指定空間大小的算法的抽象,在ehcache中預設實作了兩中算法:balanced和fromlargest。fromlargest算法比較簡單,它隻需要周遊所有的poolablestore,每次選擇一個沒有evict過的使用空間最大的poolablestore,嘗試從它evict出指定大小的空間直到指定大小的空間被騰出來;balanced算法有點複雜,它先打亂poolablestore,然後周遊亂序的poolablestore,每次選取其後部分poolablestore,對齊按一下算法排序,周遊排序後的子poolablestore集,對每個poolablestore嘗試evict指定大小的空間,直到evict執行成功。
/*
* the code below is a simplified version of this:
*
* float meanentrysize = bytesize / countsize;
* float accessrate = hitrate + missrate;
* float filllevel = hitrate / accessrate;
* float deltafilllevel = filllevel / bytesize;
* return meanentrysize * accessrate * deltafilllevel * hitdistributionfunction(filllevel);
*/
因為在poolablestore中将從磁盤evict和從記憶體evict定義成兩個不同的方法,因而對每種算法的poolevictor都由兩個子類實作:balancedaccessondiskpoolevictor、balancedaccessonheappoolevictor和fromlargestcacheondiskpoolevictor、fromlargestcacheonheappoolevictor。這裡poolablestore接口的抽象用于在提供evict操作時的資訊,如poolevictor中evict方法的實作、balanced算法中的一些統計資訊的獲得等:
public interface poolablestore extends store {
boolean evictfromonheap(int count, long size);
boolean evictfromondisk(int count, long size);
float getapproximatediskhitrate();
float getapproximatediskmissrate();
long getapproximatediskcountsize();
long getapproximatediskbytesize();
float getapproximateheaphitrate();
float getapproximateheapmissrate();
long getapproximateheapcountsize();
long getapproximateheapbytesize();
poolablestroe中evict邏輯的實作
所謂evict就是使用配置的evict算法選出部分element執行個體,将它們從store中移除。對memorystore,它隻實作evictfromonheap方法,而對diskstore隻需實作evictfromondisk方法。
對memorystore,evict操作的主要流程是根據配置的evictpolicy選取下一個expired或要被evict的element,将這個element移除,并出發expired或evict事件,在做evict之前先判斷該element或目前store處于pinned狀态,如果是,則不做evict,傳回false。因而這裡最主要的是要如何使用evictpolicy選取下一個要被evict的element。ehcache實作了四種算法:最近最少使用算法(lru)、先進先出算法(fifo)、最少使用算法(lfu)、鐘算法(clock)。
鐘算法實作比較簡單,它随即的選擇一個segment,每個segment内部儲存一個evictioniterator,每一次evict調用就是從這個iterator中擷取下一個expired element或unpinned element(如果該iterator周遊到最後一個element,則重新開始,即像鐘一樣不同的循環),将找到的element從該segment中移除。
對其他的算法,都要先從memorystore中選取一個element的樣本數組,然後使用不同的policy實作擷取樣本中的候選evict element。樣本element數組的最大容量是30,其選取算法是:如果目前evict是因為新添加一個element引起,則從新添加的element所在的segment中選取樣本,否則随機的選取一個segment,在選取的segment中随機的選取一個hashentry鍊,将這個鍊中所有unpinned element加入的樣本資料中,如果一條鍊不夠,則循環的查找下一條鍊直到樣本量達到指定的要求或整個segment所有unpinned element都已經添加到樣本中。所有的算法都是基于這些樣本選擇下一個候選的evict element。
fifopolicy:樣本中update(create)時間最早的element。
lfupolicy:樣本中最少被使用的element(hit count最少)。
lrupolicy:樣本中最近最少被使用的element(lastaccesstime最小)。
對diskstore,evict操作類似memorystore,先找到一個disksubstitute(必須是diskmarker類型)樣本數組(算法和memorystore中找element樣本數組類似,最大樣本容量也是30),對找到的樣本數組采用最少使用算法(hit count)或根據傳入要被evict的key作為下一個evict的候選,并嘗試将該disksubstitute(diskmarker)從磁盤中移除,讀取磁盤中的資料并反序列化成element,傳回凡序列化後的element執行個體。移除的步驟包括:将他從memorystore中移除;将disksubstitute對應的節點從hashentry中删除;釋放該element原本在磁盤中占用的空間;釋放disk pool中占用的空間;釋放heap pool中占用的空間。