缓存( 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 中的缓存数据,而其他服务器则无从得知这种改变,这就需要一种分布式的缓存机制。
到目前可以发现 , 整个 Jive 系统其实是围绕 Forum 、 ForumThread 和 ForumMessage 等这些主要对象展开的读取、修改或创建等操作。由于这些对象原先持久化保存在数据库中,为了提高性能和加强安全性, Jive 在这些对象外面分别实现两层包装,如图 3-7 所示。 客户端如果需要访问这些对象,首先要经过它们的代理对象。进行访问权限的检查,然后再从缓存中获取该对象。只有缓存不存在时,才会从数据库中获取。 这套机制是大多数应用系统都面临的必须解决的基本功能,因此完全可以做成一个通用的可重复使用的框架。这样在具体应用时,不必每个应用系统都架设开发这样的机制。其实 EJB 就是这样一套框架,实体 Bean 都由缓存机制支持,而通过设定 ejb-jar.xml 可以实现访问权限控制,这些工作都直接由 EJB 容器实现了,不必在代码中自己来实现。剩余的工作是调整 EJB 容器的参数,使之适合应用系统的具体要求,这些将在以后章节中讨论。 在 Jive 中,图 3-7 的机制是通过不同方式实现的。基本上是一配二模式:一个对象有一个缓冲对象和一个代理对象,这样做的一个缺点是导致对象太多,系统变得复杂。这点在阅读 Jive 源码时可能已经发现。 如果建立一个对象工厂,工厂内部封装了图 3-7 机制实现过程,客户端可以根据不同的工厂输入参数获得具体不同的对象。这样也许代码结构要更加抽象和紧凑, Java 的动态代理 API 也许是实现这个工厂的主要技术基础。有兴趣者可以进一步研究提炼。 |
|
来自: bluecrystal > 《java》