1、为什么要使用缓存系统
2、Openfire中缓存的使用
3、缓存系统的回收机制
如果不想使用某个缓存,可以去销毁它。例如某个插件创建了一个缓存,当 它卸载的时候,不再需要这个缓存,那么就可以调用destroyCache函数,去销毁一个缓存。 destroyCache的代码如下:
public static synchronized void destroyCache(String name) { // 首先从hash表中删除缓存 Cache cache = caches.remove(name); if (cache != null) { if (localOnly.contains(name)) { // 这里与集群缓存有关,我们没有讲集群缓存,所以现在这里就省去了。 localOnly.remove(name); localCacheFactoryStrategy.destroyCache(cache); } else { // 通过某种策略回收缓存 cacheFactoryStrategy.destroyCache(cache); } } }
上面的代码,引入一个题外话,就是集群缓存,localOnly表示本地独有的意思。它的反 义就是除了这个openfire,另外的openfire也有这个缓存的意思。
什么情况下,多个openfire会存在相同的缓存呢?例如,2个openfire,需要分享每个 openfire上在线的用户的时候,就可以使用相同名字的缓存来记录2个openfire上都在线的用 户。当然,同一个名字的缓存在2台openfire上都有,而且需要保持同步。
不知道您理解了没有呢?由于篇幅关系,我们只能说这么多了,更多信息,请看后面的课 程。
上面讲了这么多,但是有一个关键的没有讲到,缓存怎么创建,怎么删除。 合起来,可以统称为缓存的管理策略。
缓存的管理策略可以由开发人员自己设计,openfire允许我们自己定义缓存的管理策略。 其实,所谓管理,我们可以分解为创建、删除、回收等。
我前面说了,缓存有可能存在于一个openfire中,也可能存在于多个openfire中,并且多 个openfire可以共享缓存。从缓存可以存在于一个openfire还是多个openfire这一点分类, 我们可以把缓存管理策略分为单机缓存管理策略和集群缓存管理策略。
无论是单机缓存管理策略还是集群缓存管理策略,我们将缓存管理的共性抽象成了一个接 口类CacheFactoryStrategy。
CacheFactoryStrategy这个接口主要用来定义单机或者集群的时候,怎么去创建、销毁缓 存。CacheFactoryStrategy只是定义了管理缓存的接口,但是并没有真正的去实现缓存管理 。
真正的实现在ClusteredCacheFactory和DefaultLocalCacheStrategy。前者实现集群的缓 存管理策略,后者实现单机的缓存管理。
如果您对上面的两种缓存管理不满意,那么可以实现CacheFactoryStrategy中的接口,自 己弄一个缓存管理出来。
下面,我们来详细的解释一下缓存管理策略接口(CacheFactoryStrategy)中的方法。
// 如果集群已经启动,返回 true boolean startCluster(); // 停止集群中的一个cluster,一个cluster中可能有大于1个节点。如果节点不在cluster中 运行,那么该函数什么也不应该做 void stopCluster(); // 创建一个名为name的缓存块 Cache createCache(String name); // 销毁或者回收一个缓存块 void destroyCache(Cache cache); // 如果当前的openfire是集群的主节点,返回true。当没有在集群中运行,也返回true boolean isSeniorClusterMember(); // 返回集群中所有节点的节点信息,当不是在集群中运行的时候,返回null Collection<ClusterNodeInfo> getClusterNodesInfo(); // 得到一个集群中,最大允许的节点个数。 当是单机的时候,返回0 int getMaxClusterNodes(); // 返回集群中主节点的标识 byte[] getSeniorClusterMemberID(); // 返回节点在集群中的标识ID byte[] getClusterMemberID(); // 返回集群的伪同步时间。集群中的节点,在时间上很可能有细微的差别 public long getClusterTime(); // 告诉集群中其他cluster中的节点,执行某个任务。这个通常用来通知其他节点有缓存数 据变化了。 void doClusterTask(final ClusterTask task); // 告诉集群某个cluster中某个节点执行一个任务 void doClusterTask(ClusterTask task, byte[] nodeID); // 同步的通知集群的其他节点执行某个任务,只有当通知的集群节点执行完任务后,才返回 。由于是同步的,所以非常花费时间。建议不是必要的情况,不要使用这种方式在集群中同 步信息。 Collection<Object> doSynchronousClusterTask(ClusterTask task, boolean includeLocalMember); // 同步信息到集群中的某个节点。 Object doSynchronousClusterTask(ClusterTask task, byte[] nodeID); // 更新参数中的缓存块的统计信息,例如缓存块的大小、命中率等 void updateCacheStats(Map<String, Cache> caches); // 锁定一个缓存,通过一个关键字key Lock getLock(Object key, Cache cache); // 得到集群插件的名字,openfire只定义了实现集群的一些接口,但是并没有真正实现集 群,集群需要自己通过插件来实现,所以这里可以获得插件的名字。 String getPluginName(); // 返回某个集群节点的信息 ClusterNodeInfo getClusterNodeInfo(byte[] nodeID);
好了,上面代码中讲了CacheFactoryStrategy接口的所有方法。
DefaultLocalCacheStrategy实现了单台openfire分配管理缓存的操作,本节,对 DefaultLocalCacheStrategy类的一些函数进行分析。
创建缓存使用createCache函数,这个函数会根据系统属性中缓存的大小和生命周期创建 缓存。代码如下:
public Cache createCache(String name) { long maxSize = CacheFactory.getMaxCacheSize(name); long lifetime = CacheFactory.getMaxCacheLifetime(name); return new DefaultCache(name, maxSize, lifetime); }
注意DefaultLocalCacheStrategy中的createCache会被CacheFactory中的createCache函数使用。
销毁某一个缓存,直接调用Cache中的clear方法即可,DefaultLocalCacheStrategy封装了这个函数,代码如下;
public void destroyCache(Cache cache) { cache.clear(); }
对于单机缓存策略来说,没有办法得到集群中的节点信息,所以这里返回一个空集合。
因为是单机,没有集群,所以集群中的节点为0
public int getMaxClusterNodes() { return 0; }
获得服务器的时间,因为是单机,所以直接获得系统时间即可
public long getClusterTime() { return System.currentTimeMillis(); }
得到插件的名字,这里是单机,顺便返回一个local作为标识。
public String getPluginName() { return "local"; }
Ok,单机缓存的生成策越,我们暂时讲到这里。由于我们没有涉及到集群,所以集群相关的函数,就不用实现了。完整代码如下所示,请大家仔细看一下。
package org.jivesoftware.util.cache; import org.jivesoftware.openfire.cluster.ClusterNodeInfo; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * CacheFactoryStrategy for use in Openfire. It creates and manages local caches, and it's cluster * related method implementations do nothing. * * @see Cache * @see CacheFactory */ public class DefaultLocalCacheStrategy implements CacheFactoryStrategy { /** * Keep track of the locks that are currently being used. */ private Map<Object, LockAndCount> locks = new ConcurrentHashMap<>(); public DefaultLocalCacheStrategy() { } @Override public boolean startCluster() { return false; } @Override public void stopCluster() { } @Override public Cache createCache(String name) { // Get cache configuration from system properties or default (hardcoded) values long maxSize = CacheFactory.getMaxCacheSize(name); long lifetime = CacheFactory.getMaxCacheLifetime(name); // Create cache with located properties return new DefaultCache(name, maxSize, lifetime); } @Override public void destroyCache(Cache cache) { cache.clear(); } @Override public boolean isSeniorClusterMember() { return true; } @Override public Collection<ClusterNodeInfo> getClusterNodesInfo() { return Collections.emptyList(); } @Override public int getMaxClusterNodes() { return 0; } @Override public byte[] getSeniorClusterMemberID() { return null; } @Override public byte[] getClusterMemberID() { return new byte[0]; } @Override public long getClusterTime() { return System.currentTimeMillis(); } @Override public void doClusterTask(final ClusterTask task) { } @Override public void doClusterTask(ClusterTask task, byte[] nodeID) { throw new IllegalStateException("Cluster service is not available"); } @Override public Collection<Object> doSynchronousClusterTask(ClusterTask task, boolean includeLocalMember) { return Collections.emptyList(); } @Override public Object doSynchronousClusterTask(ClusterTask task, byte[] nodeID) { throw new IllegalStateException("Cluster service is not available"); } @Override public void updateCacheStats(Map<String, Cache> caches) { } @Override public String getPluginName() { return "local"; } @Override public Lock getLock(Object key, Cache cache) { Object lockKey = key; if (key instanceof String) { lockKey = ((String) key).intern(); } return new LocalLock(lockKey); } private void acquireLock(Object key) { ReentrantLock lock = lookupLockForAcquire(key); lock.lock(); } private void releaseLock(Object key) { ReentrantLock lock = lookupLockForRelease(key); lock.unlock(); } private ReentrantLock lookupLockForAcquire(Object key) { synchronized(key) { LockAndCount lac = locks.get(key); if (lac == null) { lac = new LockAndCount(new ReentrantLock()); lac.count = 1; locks.put(key, lac); } else { lac.count++; } return lac.lock; } } private ReentrantLock lookupLockForRelease(Object key) { synchronized(key) { LockAndCount lac = locks.get(key); if (lac == null) { throw new IllegalStateException("No lock found for object " + key); } if (lac.count <= 1) { locks.remove(key); } else { lac.count--; } return lac.lock; } } private class LocalLock implements Lock { private final Object key; LocalLock(Object key) { this.key = key; } @Override public void lock(){ acquireLock(key); } @Override public void unlock() { releaseLock(key); } @Override public void lockInterruptibly(){ throw new UnsupportedOperationException(); } @Override public Condition newCondition(){ throw new UnsupportedOperationException(); } @Override public boolean tryLock() { throw new UnsupportedOperationException(); } @Override public boolean tryLock(long time, TimeUnit unit) { throw new UnsupportedOperationException(); } } private static class LockAndCount { final ReentrantLock lock; int count; LockAndCount(ReentrantLock lock) { this.lock = lock; } } @Override public ClusterNodeInfo getClusterNodeInfo(byte[] nodeID) { // not clustered return null; } }
我们前面说过,缓存的策略是可以自己实现的。例如,openfire运行在一台内存128G的工作站中(顺便普及一下,这种工作站大约8w元一台),就可以使用一种特殊的缓存分配策略。我们来假想一下这种缓存策略,假设缓存占用100G内存前,都是不需要回收缓存的。那么这种策略需要自己实现一下,因为默认的缓存策略是不支持的。
当我们实现了这个缓存分配策略,只需要添加或者更改一个系统属性,这个系统属性是: cache.clustering.local.class 把这个属性变为自己实现的缓存分配策略类,如 org.jivesoftware.util.cache.MyLocalCacheStrategy
然后重启一下openfire,就可以使用新策略来分配缓存了。重启的过程中,cache.clustering.local.class这个属性会被CacheFactory读到,并实例化缓存策略类,代码如下:
static { localCacheFactoryClass = JiveGlobals.getProperty(LOCAL_CACHE_PROPERTY_NAME, "org.jivesoftware.util.cache.DefaultLocalCacheStrategy"); .... }
这样,以后大家就可以使用新的缓存分配策略了。
每一个需要缓存的对象,最终被包裹在CacheObject中,您可以在DefaultCache.java中找到这个类的实现。这个类,接受一个泛型V,V就是需要缓存的任意对象。
CacheObject的代码如下,详细解释请看代码中的注释:
private static class CacheObject<V> { // 需要缓存的对象 public V object; // object这个对象的字节大小 public int size; // 最近访问的对象在链接中的节点 public LinkedListNode<?> lastAccessedListNode; // 到期的缓存对象引用, public LinkedListNode<?> ageListNode; // 对象被读的次数,用于统计该对象使用的频率 public int readCount = 0; // 构造函数 public CacheObject(V object, int size) { this.object = object; this.size = size; } }
下面我们详细解释一下上面的代码:
1、lastAccessedListNode表示的是最近访问的缓存对象的链表引用。有什么用处后面再说,这里只需要记住这个单词就可以了。
2、ageListNode表示生命周期即将到了缓存对象列表的引用。我们后面也会详细解释。
前面已经讲了什么是缓存块,不懂,回到前面看一下。它的构造函数是:
public DefaultCache(String name, long maxSize, long maxLifetime)
name是缓存块的名字。
maxSize是缓存块的最大大小,以字节为单位
maxLifetime是缓存块中每个缓存对象的生命周期。如果这个值为-1,那么表示永不需要删除,它是永生的。 为了更深入的分析,我们将DefaultCache的完整代码列出如下:
public DefaultCache(String name, long maxSize, long maxLifetime) { this.name = name; this.maxCacheSize = maxSize; this.maxLifetime = maxLifetime; // 一个名字的缓存块,默认可以存放103个缓存对象 map = new HashMap<K, CacheObject<V>>(103); // 最新访问的列表 lastAccessedList = new org.jivesoftware.util.LinkedList<K>(); // 到期对象的列表 ageList = new org.jivesoftware.util.LinkedList<K>(); }
注意这里的lastAccessedList、ageList 和上一节CacheObject中的lastAccessedList、ageList ,其实是指的同一个东西,每个缓存块有一个lastAccessedList和ageList ,缓存对象中为了能够快速访问到lastAccessedList和ageList ,存了它们的引用。
向缓存块中放入缓存对象,使用put函数,它的原型是:
public synchronized V put(K key, V value)
参数key是缓存对象的关键字,或者说是在hash表中的key。如果您越看越糊涂,那么打开eclipse,找到DefaultCache.java类,这样可能会好点:)
参数value是缓存对象。
为了更清楚的讲解,我们将put函数的代码列举如下,大家可以先看一下注释,如果对注释有疑惑,那么再看一下代码后面的解释。
public synchronized V put(K key, V value) { // 如果缓存块中有该缓存对象,先删除 V answer = remove(key); // 计算缓存对象的字节大小,关于怎么计算一个对象的大小,这里有很有趣的地方,我们呆会会看一下 int objectSize = 1; try { objectSize = CacheSizes.sizeOfAnything(value); } catch (CannotCalculateSizeException e) { Log.warn(e.getMessage(), e); } // 如果缓存对象太大,大于缓存块最大容量的90%,那么就不要将该缓存对象放在缓存中了。 if (maxCacheSize > 0 && objectSize > maxCacheSize * .90) { Log.warn("Cache: " + name + " -- object with key " + key + " is too large to fit in cache. Size is " + objectSize); return value; } // 设置最新的缓存块中的缓存大小 cacheSize += objectSize; // 创建一个缓存对象 DefaultCache.CacheObject<V> cacheObject = new DefaultCache.CacheObject<V>(value, objectSize); // 将创建的缓存对象放到缓存块中 map.put(key, cacheObject); // 将创建的缓存对象,放到最近访问链表的最前面,并返回加入链表的这个节点, LinkedListNode<K> lastAccessedNode = lastAccessedList.addFirst(key); // 将cacheObject.lastAccessedListNode指向“最近访问链表”中的刚加入的节点。其实就是自己所在的节点。 cacheObject.lastAccessedListNode = lastAccessedNode; // 将自己加入“过期链表”的首部 LinkedListNode<K> ageNode = ageList.addFirst(key); // 当前节点的出生时间 ageNode.timestamp = System.currentTimeMillis(); // 将cacheObject.ageListNode指向“过期链表”中的当前节点。 cacheObject.ageListNode = ageNode; // 调整缓存占用的空间,如果内存占用过多,那么将一些缓存对象从内存中清除。 cullCache(); return answer; }
cullCache负责缓存对象的回收,如果缓存块中占用的空间已经大于它的最大空间的97%,那么就需要删除一些最不常用的缓存对象,直到剩余空间至少有10%。ok,明白了大致的算法,我们来看一下cullCache的实现吧。
protected final void cullCache() { // 如果缓存块是不限制大小的,那么直接返回,不做内存回收。 if (maxCacheSize < 0) { return; } // 如果内存占用大于97%,那么执行回收操作。 int desiredSize = (int)(maxCacheSize * .97); if (cacheSize >= desiredSize) { // 删除一些生命周期到了的缓存对象 deleteExpiredEntries(); desiredSize = (int)(maxCacheSize * .90); // 如果缓存块占用的内存还大于其允许的最大内存的90%,那么继续删除。 if (cacheSize > desiredSize) { long t = System.currentTimeMillis(); do { // 按照顺序移出最早访问的对象,也就是lastAccessedList链表的队尾缓存对象,那个对象是很久没有使用的 remove(lastAccessedList.getLast().object); } while (cacheSize > desiredSize); // 这里计算出了将缓存块对象所占内存缩小到90%所用的时间。 t = System.currentTimeMillis() - t; Log.warn("Cache " + name + " was full, shrinked to 90% in " + t + "ms."); } } }
还记得上面的put函数中有一个计算对象大小的函数CacheSizes.sizeOfAnything吗?
有的同学问,为什么要计算对象的大小啊,其实上面的很多文字中已经解释了这个问题。如果您一路读来,还是没有理解,我觉得可能是您需要停下来好好思考一下,呼吸一下新鲜空气。以致于您的大脑细胞不要因为缺氧牺牲得太多。这点很重要,据说脑细胞是一种不可再生的细胞,死了就死了,不会有新细胞产生了,所以人会越来越傻,到了100岁很可能就老年痴呆。
除了呼吸新鲜空气,还有什么办法让脑细胞死得少点,我的经验告诉我,要有正确的学习方法。 在我达到现在的水平之前,我读了很多书、文章、教程和源码,让我有足够的能力来学习新知识。同样,你也可以通过学习我们的课程,练好内功,学习什么都快了,吃什么也香了,当然就有更多的时间享受人生了。何必为了一个bug、一个错误浪费一下午的阳光呢。
废话不多说,开始我们本节的内容。我们首先来明确一下,一些基础类型占用的空间,然后才能计算一个复杂对象占用的空间。因为复杂对象是基础类型组成的。
复杂对象的空间是基础类型的空间之和。在java中,基础类型的占用的空间如下:
int类型4个字节。
char用unicode表示是2个字节。
boolean值是1个字节
Long是8个字节
Double是8个字节
Date是12个字节
Object的引用是4个字节。
String占用的字节是4 + string.getBytes().length,4是字符串引用占用的空间。
sizeOfAnything函数用来计算参数所占用的内存空间,sizeOfAnything函数代码如下,详细解释请看注释。
// 如果object对象不能计算,抛出异常CannotCalculateSizeException public static int sizeOfAnything(Object object) throws CannotCalculateSizeException { // 如果对象为null,那么占用0字节 if (object == null) { return 0; } // 如果是继承Cacheable,那么可以直接获得对象的大小 if (object instanceof Cacheable) { return ((Cacheable)object).getCachedSize(); } // 如果是字符串类型,返回字符串的大小 else if (object instanceof String) { return sizeOfString((String)object); } // 如果是Long类型,返回Long的大小,这里应该是bug,应该返回sizeOfObject() + sizeOfLong(); else if (object instanceof Long) { return sizeOfLong(); } // 如果是INT类型,返回INT的大小 else if (object instanceof Integer) { return sizeOfObject() + sizeOfInt(); } // 同上 else if (object instanceof Double) { return sizeOfObject() + sizeOfDouble(); } // 同上 else if (object instanceof Boolean) { return sizeOfObject() + sizeOfBoolean(); } // MAP的大小 else if (object instanceof Map) { return sizeOfMap((Map)object); } // 如果是long数组,这里考虑得很周到。 else if (object instanceof long[]) { long[] array = (long[])object; return sizeOfObject() + array.length * sizeOfLong(); } // 集合的大小 else if (object instanceof Collection) { return sizeOfCollection((Collection)object); } // byte类型的大小 else if (object instanceof byte[]) { byte [] array = (byte[])object; return sizeOfObject() + array.length; } // 这里是本段代码的精华,可以计算任意类型,但是,这仅仅是不得以而为之,因为效率很低 else { int size = 1; try { // 通过计算输出流的方式,计算输出流的大小 CacheSizes.NullOutputStream out = new NullOutputStream(); ObjectOutputStream outObj = new ObjectOutputStream(out); outObj.writeObject(object); size = out.size(); // 这就是写入到输出流中的大小 } catch (IOException ioe) { throw new CannotCalculateSizeException(object); } return size; } }
注意,上面代码计算对象所占用的空间,最好的情况是,大家自己实现接口Cacheable的getCachedSize函数,因为只有大家最清楚,您的数据准确的占用空间。写到这里,我不得不佩服openfire开发人员的精悍编程技术。 通过学习本课程,我相信您也能做到
好了,如果您将本课读懂,而且认真的去看了若干次org.jivesoftware.util.cache包下的源码,那么我可以负责任的告诉您,您现在对openfire缓存系统的理解,应该非常牛逼了。
通过本章的学习,仅仅只是想告诉大家一个道理。工作学习要深入,不要浅尝而止,不要觉得自己有很多机会去提高,也许时间会告诉您,您一生提高的机会并不多。
遇到一部好教程的机会也不多,好了,最后感谢大家的阅读。