技术开发 频道

JAVA缓存研究之剖析Jive的缓存机制

【IT168 技术文档】

   Jive是一个广受欢迎的开放源码的论坛项目,虽然推出了很多年,但至今很多Java程序员还对它津津乐道。从框架结构上看,它采用了很多设计模式,如Factory模式、Proxy模式、Decorator模式、Iterator模式,使得程序易于扩展和移植。从设计细节上看,它采用了很多先进的设计思想和方法,如XML读写配置文件、数据库的缓存和连接池、帖子的过滤和TreeWalk遍历等,使得程序更加强健和高效。本文主要谈的是Jive缓存机制的实现。
 
简介
 
  大家知道,在两个存取速度差距很大的对象(比如数据库和内存)之间,通常要加一个缓存来匹配二者的速度。因此,缓存机制在实际项目中还是经常遇到的。同样Jive也使用缓存来加快贴子的显示。如果试图编写一个类似的程序,不妨研究一下Jive源码,可能对你大有帮助。
 
  在Jive 2.1.2中,涉及Jive缓存机制的Java类大致可以分为以下四个部分(为了简化起见,本文只讨论帖子缓存机制的实现。用户名和权限的存取虽然也用到了缓存,但其实现机制与前者类似,因此不再赘述):
 
  第一部分,提供HashMap、LinkedListedlist等数据结构,以便实现缓存机制,其中HashMap是JDK提供的,其Key类型为Object。可以在com.jivesoftware.util包中找到这些数据结构。此部分包括Cache类、 LinkedList类、LinkedListNode类、Casheable接口、CacheObject类、CacheableBoolean类、CacheableInt类、CacheableLong类、CacheableLongArray类、CacheableString类、CacheSizes类、CacheTimer类。
 
  第二部分,提供LongHashMap、LongLinkedListedlist等数据结构以实现缓存机制。与第一部分不同的是,它的HashMap是自己编写的,其Key为Long型,因此被冠以LongHashMap的名称。同样可以在com.jivesoftware.util包中找到它们。该部分包括LongHashMap类、LongCache类、 LongCacheObject类、LongLinkedList类和LongLinkedListNode类。还有第一部分中的Casheable接口,它的各种数据类型的实现、CacheSizes类和CacheTimer类,也可归于这部分。它们可看作是第一部分和第二部分的交集。
 
  第三部分,调用底层数据结构以提供论坛对象的缓存。可以在com.jivesoftware.forum.database包中找到这些底层数据结构。该部分包括的类主要有DatabaseCacheManager类、DbForumFactory类、DbForum类、DbForumThread类、DbForumMessage 类、DatabaseCache类、ForumCache类、 ForumThreadCache类和ForumMessageCache类。
 
  第四部分,向Jsp页面提供访问接口,同样可以在com.jivesoftware.forum.database包中找到这些接口。该部分包括的类有ForumThreadBlockIterator类和ForumMessageBlockIterator类,第三部分的DbForum类、DbForumThread类和DbForumMessage 类也可以包括进来。实际上,这三个类是第三部分和第四部分联系的纽带。在com.jivesoftware.util包中还有一个LongList类,它用来将ForumThreadBlockIterator类和ForumMessageBlockIterator类转化成Long型数组,因此也应算在这部分。
 
  从上面介绍可看出,缓存机制也可以划分为三层,即第一和第二部分的底层数据结构,第三部分的中间层和第四部分的上层访问接口,下面分别讨论它们。
 
底层数据结构
 
  Jive缓存机制的原理其实很简单,就是把所要缓存的对象加到HashMap哈希映射表中,用两个LinkedListedlist双向链表分别维持着缓存对象和每个缓存对象的生命周期。如果一个缓存对象被访问到,那么就把它放到链表的最前面,然后不定时地把要缓存的对象加入链表中,把过期对象删除,如此反复。实际上比较第一和第二部分就可以发现,它们的代码几乎完全相同。差别就在第二部分的哈希映射表没有采用JDK提供的类,而是采用了作者自己编写的一个类,将原来哈希映射表的Key类型由Object改为Long。这样做虽然在一定程度上加快了缓存的速度,并减小了缓存的大小,但无形之中也减低了程序的稳定性和可读性,因此不推荐仿效。值得一提的是,在Jive 1.0.2版中,所有Forum、Thread、Message的ID和它们内容的缓存都是用第一部分的Java类实现的。它在升级到后面的版本时,其内容采用了第二部分的Java类实现,但其ID仍用第一部分的Java类实现,这是Jive中值得注意的一个地方。下面先来看第一部分的Java类实现。LinkedListNode类的源码为:
 
public class LinkedListNode {
 public LinkedListNode previous;
 public LinkedListNode next;
 public Object object;
 public long timestamp;
 
 public LinkedListNode(Object object,
   LinkedListNode next,LinkedListNode previous){
 this.object = object;
 this.next = next;
 this.previous = previous;
 }
 public void remove() {
 previous.next = next;
 next.previous = previous;
 }
 public String toString() {
 return object.toString();
 }
}
 
  很明显,这是一个双向链表的节点类,previous、next分别记录前后节点的指针,object用于记录所需缓存的对象,timestamp用于记录当前节点被创建时的时间戳。当该时间戳超过该节点的生存周期时,它就会被remove()方法删除掉。该类主要完成的功能就是由LinkedListNode构成LinkedList链表,而由LinkedList类实现getFirst()、getLast()、addFirst()、addLast()、clear()等链表的基本方法。
 
  再来看Cacheable接口和它的一个实现类CacheableInt的源码:
 
public interface Cacheable {
 public int getSize();
}
public class CacheableInt implements Cacheable {
 private int intValue;
 public CacheableInt(int intValue) {
 this.intValue = intValue;
 }
 public int getInt() {
 return intValue;
 }
 public int getSize() {
 return CacheSizes.sizeOfObject() + CacheSizes.sizeOfInt();
 }
}
 
  从上面的代码可以看到,Cacheable接口只有一个方法getSize()。它要求所有继承类实现该方法,并输出占用缓存的大小,以便实施管理。那么为什么CacheableInt. getSize()得到的是sizeOfObject()+sizeOfInt()呢?因为任何类都继承自Object,计算空间时当然也要把它算上了。
 
  还有一个CacheObject类,它是缓存的基本元素,来看一下它的代码:
 
public final class CacheObject {
 public Cacheable object;
 public int size;
 public LinkedListNode lastAccessedListNode;
 public LinkedListNode ageListNode;
 
 public CacheObject(Cacheable object, int size) {
 this.object = object;
 this.size = size;
 }
}
 
  lastAccessedListNode记录着一个缓存节点的Key值,是构成lastAccessedList链表的基本元素,在lastAccessedList链表中,经常被访问到的节点总是在最前面。ageListNode记录着缓存节点的加入时间,是构成ageList链表的基本元素。而ageList链表是按时间先后排序,先加入的节点总是在最后面。lastAccessedListNode和ageListNode本来可以分写成两个类,毕竟lastAccessedListNode并不需要ageListNode的成员变量timestamp,但是为了简化程序,Jive把它们写成了一个类。这也是值得注意的一个地方。
 
  现在来看缓存机制中最关键的一个类Cache的部分代码,其中主要是add()和get()方法。有关这两个方法的介绍请参考代码中的注释。
 
public class Cache implements Cacheable {
 protected static long currentTime = CacheTimer. currentTime;
 protected HashMap cachedObjectsHash;
 protected LinkedList lastAccessedList;
 protected LinkedList ageList;
 //缓存元素的最大尺寸128KB,可修改
 protected int maxSize = 128 * 1024;
 //整个缓存的大小
 protected int size = 0;
 //缓存元素的最大保存时间,用Cache(long maxLifetime)初始化
 protected long maxLifetime = -1;
 //记录cache的命中次数和未命中次数
 protected long cacheHits, cacheMisses = 0L;
 ......
 //向哈希表中添加一个关键字为Key的缓存对象object
 public synchronized void add(Object key, Cacheable object) {
 //先把原来的对象remove掉
 remove(key);
 int objectSize = object.getSize();
 //如果对象太大,则不加入缓存
 if (objectSize > maxSize * .90) {
   return;
 }
 size += objectSize;
 //新建一个缓存对象,并放入哈希表中
 CacheObject cacheObject = new CacheObject(object, objectSize);
 cachedObjectsHash.put(key, cacheObject);
 // 把缓存元素的Key放到lastAccessed List链表的最前面
 LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
 cacheObject.lastAccessedListNode = lastAccessedNode;
 //把缓存元素的Key放到ageList链表的最前面,并记下当前时间
 LinkedListNode ageNode = ageList.addFirst(key);
 ageNode.timestamp = System.currentTimeMillis();
 cacheObject.ageListNode = ageNode;
 // 在cullCache()中,先调用deleteExpiredEntries()把过期对象删掉,
 如果缓存还是太满,则调用 remove(lastAccessedList.getLast().object)把
 lastAccessedList中不常访问的对象删掉
 cullCache();
 }
 //在哈希表中得到一个关键字为Key的缓存对象object
 public synchronized Cacheable get(Object key) {
 // 清理过期对象
 deleteExpiredEntries();
 CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
 if (cacheObject == null) {
   //没找到则未命中次数加一
   cacheMisses++;
   return null;
 }
 
 //找到则命中次数加一
 cacheHits++;
 //将该缓存对象从lastAccessedList链表中取下并插入到链表头部
 cacheObject.lastAccessedListNode.remove();
 lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
 return cacheObject.object;
 }
}
  到这里,第一部分的Java类实现就介绍完了。正如上文提到的那样,第二部分的Java类实现与第一部分基本上没有什么差别,因此就不再赘述。
0
相关文章