Jive的缓存机制
Jive论坛的一个主要特点就是其性能速度快,因此很多巨大访问量的网站都采用了Jive论坛。这些都是由于Jive采取了高速缓存机制。

缓存(Cache)机制是提高系统运行性能必不可少的技术。缓存机制从原理上讲比较简单,就是在原始数据第一次读取后
保存在内存中,下次读取时,就直接从内存中读取。原始数据有可能保存在持久化介质或网络上。缓存机制也是
代理模式的一种实现。

 缓存原理和实现
Jive的Cache总体来说实现得不是非常精简和有效。它是针对每个具体数据对象逐个实现缓冲,这种“穷尽”的办法不是实
践所推荐的用法。通过使用动态代理模式,可以根据具体方法的不同来实现缓存是值得推荐的做法。Jive的缓存实现
得比较简单,可以用来学习和研究缓存机制。

Jive中的Cache实现了缓存机制的大部分行为,它是将对象用惟一的关键字Key作标识保存在HashMap或Hashtable中。
当然,必须知道这些对象的大小,这个前提条件的设定可以保证缓存增长时不会超过规定的最大值。

如果缓存增长得太大,一些不经常被访问的对象将首先从缓存中删除。如果设置了对象的最大生命周期时间,即使这个
对象被反复频繁访问,也将从缓存中删除。这个特性可以适用于一些周期性需要刷新的数据,如来自数据库的数据。

在Cach中除了getObject()方法的性能依据缓存大小,其他方法的性能都是比较快的。一个HashMap用来实现快速寻找,
两个LinkedList中一个以一定的访问顺序来保存对象,叫accessed LinkedList;另外一个以它们加入缓存的顺序保存
这些对象,这种保存对象只是保存对象的引用,叫 age LinkedList。注意,这里的LinkedList不是JDK中的LinkedList
,而是Jive自己定义的LinkedList。

当对象被加入缓存时,首先被CacheObject封装。封装有以下信息:对象大小(以字节计算),一个指向accessed LinkedList的
引用,一个指向age LinkedList的引用。

当从缓存中获取一个对象如ObjectA时,首先,HashMap寻找到指向封装ObjectA等信息的CacheObject对象。然后,这个
对象将被移动到accessed LinkedList的前面,还有其他一些动作如缓存清理、删除、过期失效等都是在这个动作中一起
触发实现的。



public class Cache implements Cacheable {
    
/**
     * 因为System.currentTimeMillis()执行非常耗费性能,因此如果get操作都执行
     * 这条语句将会形成性能瓶颈, 通过一个全局时间戳来实现每秒更新
     * 当然,这意味着在缓存过期时间计算上有一到几秒的误差
     
*/


    
protected static long currentTime = CacheTimer.currentTime;

    
//CacheObject对象
    protected HashMap cachedObjectsHash;

    
//accessed LinkedList 最经常访问的排列在最前面
    protected LinkedList lastAccessedList;

    
//以缓存加入顺序排列,最后加入排在最前面;越早加入的排在最后面
    protected LinkedList ageList;

    
//缓存最大限制 默认是128k 可根据内存设定,越大性能越高
    protected int maxSize =  128 * 1024;

    
//当前缓存的大小
    protected int size = 0;

    
//最大生命周期时间,默认是没有
    protected long maxLifetime = -1;

    
//缓存的击中率,用于评测缓存效率
    protected long cacheHits, cacheMisses = 0L;

    
public Cache() {
        
// 构造HashMap. 默认capacity 是11
        
// 如果实际大小超过11,HashMap将自动扩充,但是每次扩充都
        
// 是性能开销,因此期初要设置大一点

        cachedObjectsHash 
= new HashMap(103);
        lastAccessedList 
= new LinkedList();
        ageList 
= new LinkedList();
    }


    
public Cache(int maxSize) {
        
this();
        
this.maxSize = maxSize;
    }


    
public Cache(long maxLifetime) {
        
this();
        
this.maxLifetime = maxLifetime;
    }


    
public Cache(int maxSize, long maxLifetime) {
        
this();
        
this.maxSize = maxSize;
        
this.maxLifetime = maxLifetime;
    }


    
public int getSize() {        return size;    }

    
public int getMaxSize() {        return maxSize;    }

 
    
public void setMaxSize(int maxSize) {
        
this.maxSize = maxSize;
        
// 有可能缓存大小超过最大值,需要激活删除清理动作
        cullCache();
    }


    
public synchronized int getNumElements() {
        
return cachedObjectsHash.size();
    }


    
/**
     * 增加一个Cacheable对象
     * 因为HashMap不是线程安全的,所以操作方法要使用同步
     * 如果使用Hashtable就不必同步
     
*/


    
public synchronized void add(Object key, Cacheable object) {
        
// 删除已经存在的key
        remove(key);
        
int objectSize = object.getSize();
        
// 如果被缓存对象的大小超过最大值,就放弃
        if (objectSize > maxSize * .90{            return;        }
        size 
+= objectSize;
        
//创建一个CacheObject对象
        CacheObject cacheObject = new CacheObject(object, objectSize);
        cachedObjectsHash.put(key, cacheObject);  
//保存这个CacheObject
        
// 加入accessed LinkedList,Jive自己的LinkedList在加入时可以返回值
        LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
        
// 保存引用
        cacheObject.lastAccessedListNode = lastAccessedNode;
        
// 加入到age LinkedList
        LinkedListNode ageNode = ageList.addFirst(key);
        
// 这里直接调用System.currentTimeMillis();用法值得讨论
        ageNode.timestamp = System.currentTimeMillis();
        
// 保存引用
        cacheObject.ageListNode = ageNode;
        
// 做一些清理工作
        cullCache();
    }


    
/**
     * 从缓存中获得一个被缓存的对象,这个方法在下面两种情况返回空
     *    <li>该对象引用从来没有被加入缓存中
     *    <li>对象引用因为过期被清除</ul>
     
*/


    
public synchronized Cacheable get(Object key) {
        
// 清除过期缓存
        deleteExpiredEntries();
        
//以Key从缓存中获取一个对象引用
        CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
        
if (cacheObject == null{
            
// 不存在,增加未命中率
            cacheMisses++;
            
return null;
        }

        
// 存在,增加命中率
        cacheHits++;
        
// 从accessed LinkedList中将对象从当前位置删除
        
// 重新插入在第一个
        cacheObject.lastAccessedListNode.remove();
        lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
        
return cacheObject.object;
    }

    …
}

在Cache中,关键字Key是一个对象,为了再次提高性能,可以进一步将Key确定为一个long类型的整数。 

 缓存使用
建立LongCache只是为了提高原来的Cache性能,本身无多大意义,可以将LongCache看成与Cache一样的类。
LongCache的关键字Key是Forum、ForumThread以及 ForumMessage等long类型的ID,值Value是Forum、ForumThread以及
ForumMessage等的对象。这些基本是通过DatabaseCacheManager实现完成,在主要类DbForumFactory的初始化构造时,
同时构造了DatabaseCacheManager的实例cacheManager。

前面过滤器功能分析中,Message对象获得方法的第一句如下:
protected ForumMessage getMessage(long messageID, long threadID, long forumID) throws
      ForumMessageNotFoundException 
{
      DbForumMessage message 
= cacheManager.messageCache.get(messageID);
    …
}


其中,cacheManager是DatabaseCacheManager的实例,DatabaseCacheManager是一个缓存Facade类。在其中包含了5种
类型的缓存,都是针对 Jive的5个主要对象,DatabaseCacheManager主要代码如下:

public class DatabaseCacheManager {

    
public UserCache userCache;                          //用户资料缓存
    public GroupCache groupCache;                       //组资料缓存
    public ForumCache forumCache;                       //Forum论坛缓存
    public ForumThreadCache threadCache;                //Thread主题缓存
    public ForumMessageCache messageCache;          //Message缓存
    public UserPermissionsCache userPermsCache;     //用户权限缓存
    public DatabaseCacheManager(DbForumFactory factory) {
        …
        forumCache 
= 
            
new ForumCache(new LongCache(forumCacheSize, 6*HOUR), factory);
        threadCache 
= 
            
new ForumThreadCache(
                  
new LongCache(threadCacheSize, 6*HOUR), factory);
        messageCache 
= new ForumMessageCache(
                  
new LongCache(messageCacheSize, 6*HOUR), factory);
        userCache 
= new UserCache(
                  
new LongCache(userCacheSize, 6*HOUR), factory);
        groupCache 
= new GroupCache(
                 
new LongCache(groupCacheSize, 6*HOUR), factory);
        userPermsCache 
= new UserPermissionsCache(
                
new UserPermsCache(userPermCacheSize, 24*HOUR), factory
        );
    }

    …
}

从以上代码看出,ForumCache等对象生成都是以LongCache为基础构建的,以ForumCache为例,代码如下:
public class ForumCache extends DatabaseCache {
    
//以Cache构建ID缓存
    protected Cache forumIDCache = new Cache(128*10246*JiveGlobals.HOUR);

    
//以LongCache构建整个对象缓存
    public ForumCache(LongCache cache, DbForumFactory forumFactory) {
        
super(cache, forumFactory);
    }

    
public DbForum get(long forumID) throws ForumNotFoundException {
        …
        DbForum forum 
= (DbForum)cache.get(forumID);
        
if (forum == null{    //如果缓存没有从数据库中获取
            forum = new DbForum(forumID, factory);
            cache.add(forumID, forum); 
        }

        
return forum;
    }


public Forum get(String name) throws ForumNotFoundException {
    
//以name为key,从forumIDCache中获取ID
   CacheableLong forumIDLong = (CacheableLong)forumIDCache.get(name);
        
if (forumIDLong == null//如果缓存没有 从数据库获得
            long forumID = factory.getForumID(name);
            forumIDLong 
= new CacheableLong(forumID); //生成一个缓存对象
            forumIDCache.add(name, forumIDLong);
        }

        
return get(forumIDLong.getLong());
    }

    …
}

 

由此可以看到,LongCache封装了Cache的核心功能,而ForumCache等类则是在LongCache核心外又包装了与应用系统相关
的操作,这有点类似装饰(Decorator)模式。

从中也可以看到Cache和LongCache两种缓存的用法。
使用Cache时的关键字Key是任何字段。如上面代码中的String name,如果用户大量帖子主题查询中,Key是query + blockID,见
DbForum中的getThreadBlock方法;而值Value则是Long类型的ID,如ForumID或ThreadID等。


LongCache的关键字Key是Long类型的ID,如ForumID或ThreadID等;而值Value则是Forum、ForumThread或ForumMessage等
主要具体对象。

在实际使用中,大多数是根据ID获得对象。但有时并不是这样,因此根据应用区分了两种Cache,这其实类似数据库的数据
表,除了主关键字外还有其他关键字。


 小结
缓存中对象是原对象的映射,如何确保缓存中对象和原对象的一致性?即当原对象发生变化时,缓存中的对象也必须立即
更新。这是缓存机制需要解决的另外一个基本技术问题。

Jive中是在原对象发生变化时,立即进行清除缓存中对象,如ForumMessage对象的创建。在DbForumThread的AddMessage
方法中有下列语句:

factory.cacheManager.threadCache.remove(this.id);
factory.cacheManager.forumCache.remove(this.forumID);

即当有新的帖子加入时,将ForumThreadCache和ForumCache相关缓冲全部清除。这样,当有相关对象读取时,将直接从数
据库中读取,这是一种非常简单的缓存更新方式。

在复杂的系统,例如有一台以上的服务器运行着Jive系统。如果一个用户登陆一台服务器后,通过这台服务器增加新帖。
那么按照上述原理,只能更新本服务器JVM中的缓存数据,而其他服务器则无从得知这种改变,这就需要一种分布式的
缓存机制。

 


图3-7  Jive主要对象的访问
到目前可以发现,整个Jive系统其实是围绕Forum、ForumThread和ForumMessage等这些主要对象展开的读取、修改或创建
等操作。由于这些对象原先持久化保存在数据库中,为了提高性能和加强安全性,Jive在这些对象外面分别实现两层包装
,如图3-7所示。

客户端如果需要访问这些对象,首先要经过它们的代理对象。进行访问权限的检查,然后再从缓存中获取该对象。只有
缓存不存在时,才会从数据库中获取。

这套机制是大多数应用系统都面临的必须解决的基本功能,因此完全可以做成一个通用的可重复使用的框架。这样在
具体应用时,不必每个应用系统都架设开发这样的机制。其实EJB就是这样一套框架,实体Bean都由缓存机制支持,
而通过设定ejb-jar.xml可以实现访问权限控制,这些工作都直接由EJB容器实现了,不必在代码中自己来实现。剩余
的工作是调整EJB容器的参数,使之适合应用系统的具体要求,这些将在以后章节中讨论。

在Jive中,图3-7的机制是通过不同方式实现的。基本上是一配二模式:一个对象有一个缓冲对象和一个代理对象,这样
做的一个缺点是导致对象太多,系统变得复杂。这点在阅读Jive源码时可能已经发现。

如果建立一个对象工厂,工厂内部封装了图3-7机制实现过程,客户端可以根据不同的工厂输入参数获得具体不同的
对象。这样也许代码结构要更加抽象和紧凑,Java的动态代理API也许是实现这个工厂的主要技术基础。有兴趣者可以
进一步研究提炼。