每日一得

不求多得,只求一得 about java,hibernate,spring,design,database,Ror,ruby,快速开发
最近关心的内容:SSH,seam,flex,敏捷,TDD
本站的官方站点是:颠覆软件

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  220 随笔 :: 9 文章 :: 421 评论 :: 0 Trackbacks

4  Jive 的缓存机制

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

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

4.1  缓存原理和实现

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 类型的整数。

4.2  缓存使用

建立 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*1024, 6*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 ,这其实类似数据库的数据表,除了主关键字外还有其他关键字。

4.3  小结

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

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 也许是实现这个工厂的主要技术基础。有兴趣者可以进一步研究提炼。

posted on 2006-08-31 12:27 Alex 阅读(428) 评论(0)  编辑  收藏 所属分类: java

只有注册用户登录后才能发表评论。


网站导航: