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
类型的整数。
建立
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
,这其实类似数据库的数据表,除了主关键字外还有其他关键字。
缓存中对象是原对象的映射,如何确保缓存中对象和原对象的一致性?即当原对象发生变化时,缓存中的对象也必须立即更新。这是缓存机制需要解决的另外一个基本技术问题。
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
也许是实现这个工厂的主要技术基础。有兴趣者可以进一步研究提炼。