Jive学习(四)--Jive缓存
表现层的cache
1.Cache类
/** * $RCSfile: Cache.java,v $ * $Revision: 1.3 $ * $Date: 2001/10/03 21:55:12 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ package com.jivesoftware.util; import java.util.*; import com.jivesoftware.util.LinkedList; /** * General purpose cache implementation. It stores objects associated with * unique keys in memory for fast access. All objects added to the cache must * implement the Cacheable interface, which requires objects to know their * size in memory. This restrictions allows the cache to never grow larger * than a specified amount.<p> * * If the cache does grow too large, objects will be removed such that those * that are accessed least frequently are removed first. Because expiration * happens automatically, the cache makes <b>no</b> gaurantee as to how long * an object will remain in cache after it is put in. The cache will return * null if the requested object is not found.<p> * * Optionally, a maximum lifetime for all objects can be specified. In that * case, objects will be deleted from cache after that amount of time, even * if they are frequently accessed. This feature is useful if objects put in * cache represent data that should be periodically refreshed; for example, * information from a database.<p> * * Cache is optimized for fast data access. The getObject() method has 0(n) * performance regardless of cache size. The other cache operations also * perform quite fast.<p> * * Cache objects are thread safe.<p> * * The algorithm for cache is as follows: a HashMap is maintained for fast * object lookup. Two linked lists are maintained: one keeps objects in the * order they are accessed from cache, the other keeps objects in the order * they were originally added to cache. When objects are added to cache, they * are first wrapped by a CacheObject which maintains the following pieces * of information:<ul> * <li> The size of the object (in bytes). * <li> A pointer to the node in the linked list that maintains accessed * order for the object. Keeping a reference to the node lets us avoid * linear scans of the linked list. * <li> A pointer to the node in the linked list that maintains the age * of the object in cache. Keeping a reference to the node lets us avoid * linear scans of the linked list.</ul> * * To get an object from cache, a hash lookup is performed to get a reference * to the CacheObject that wraps the real object we are looking for. * The object is subsequently moved to the front of the accessed linked list * and any necessary cache cleanups are performed. Cache deletion and expiration * is performed as needed. * * @see Cacheable */ public class Cache implements Cacheable { /** * One of the major potential bottlenecks of the cache is performing * System.currentTimeMillis() for every cache get operation. Instead, * we maintain a global timestamp that gets updated once a second. This * means that cache expirations can be no more than one second accurate. */ protected static long currentTime = CacheTimer.currentTime; /** * Maintains the hash of cached objects. Hashing provides the best * performance for fast lookups. */ protected HashMap cachedObjectsHash; /** * Linked list to maintain order that cache objects are accessed * in, most used to least used. */ protected LinkedList lastAccessedList; /** * Linked list to maintain time that cache objects were initially added * to the cache, most recently added to oldest added. */ protected LinkedList ageList; /** * Maximum size in bytes that the cache can grow to. Default * maximum size is 128 K. */ protected int maxSize = 128 * 1024; /** * Maintains the current size of the cache in bytes. */ protected int size = 0; /** * Maximum length of time objects can exist in cache before expiring. * Default is that objects have no maximum lifetime. */ protected long maxLifetime = -1; /** * Maintain the number of cache hits and misses. A cache hit occurs every * time the get method is called and the cache contains the requested * object. A cache miss represents the opposite occurence.<p> * * Keeping track of cache hits and misses lets one measure how efficient * the cache is; the higher the percentage of hits, the more efficient. */ protected long cacheHits, cacheMisses = 0L; /** * Create a new cache with default values. Default cache size is 128K with * no maximum lifetime. */ public Cache() { // Our primary data structure is a hash map. The default capacity of 11 // is too small in almost all cases, so we set it bigger. cachedObjectsHash = new HashMap(103); lastAccessedList = new LinkedList(); ageList = new LinkedList(); } /** * Create a new cache and specify the maximum size for the cache in bytes. * Items added to the cache will have no maximum lifetime. * * @param maxSize the maximum size of the cache in bytes. */ public Cache(int maxSize) { this(); this.maxSize = maxSize; } /** * Create a new cache and specify the maximum lifetime of objects. The * time should be specified in milleseconds. The minimum lifetime of any * cache object is 1000 milleseconds (1 second). Additionally, cache * expirations have a 1000 millesecond resolution, which means that all * objects are guaranteed to be expired within 1000 milliseconds of their * maximum lifetime. * * @param maxLifetime the maximum amount of time objects can exist in * cache before being deleted. */ public Cache(long maxLifetime) { this(); this.maxLifetime = maxLifetime; } /** * Create a new cache and specify the maximum size of for the cache in * bytes, and the maximum lifetime of objects. * * @param maxSize the maximum size of the cache in bytes. * @param maxLifetime the maximum amount of time objects can exist in * cache before being deleted. */ public Cache(int maxSize, long maxLifetime) { this(); this.maxSize = maxSize; this.maxLifetime = maxLifetime; } /** * Returns the current size of the cache in bytes. * * @return the size of the cache in bytes. */ public int getSize() { return size; } /** * Returns the maximum size of the cache in bytes. If the cache grows too * large, the least frequently used items will automatically be deleted so * that the cache size doesn't exceed the maximum. * * @return the maximum size of the cache in bytes. */ public int getMaxSize() { return maxSize; } /** * Sets the maximum size of the cache in bytes. If the cache grows too * large, the least frequently used items will automatically be deleted so * that the cache size doesn't exceed the maximum. * * @param maxSize the maximum size of the cache in bytes. */ public void setMaxSize(int maxSize) { this.maxSize = maxSize; // It's possible that the new max size is smaller than our current cache // size. If so, we need to delete infrequently used items. cullCache(); } /** * Returns the number of objects in the cache. * * @return the number of objects in the cache. */ public synchronized int getNumElements() { return cachedObjectsHash.size(); } /** * Adds a new Cacheable object to the cache. The key must be unique. * * @param key a unique key for the object being put into cache. * @param object the Cacheable object to put into cache. */ public synchronized void add(Object key, Cacheable object) { // Delete an old entry if it exists. remove(key); int objectSize = object.getSize(); // If the object is bigger than the entire cache, simply don't add it. if (objectSize > maxSize * .90) { return; } size += objectSize; CacheObject cacheObject = new CacheObject(object, objectSize); cachedObjectsHash.put(key, cacheObject); // Make an entry into the cache order list. LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key); // Store the cache order list entry so that we can get back to it // during later lookups. cacheObject.lastAccessedListNode = lastAccessedNode; // Add the object to the age list LinkedListNode ageNode = ageList.addFirst(key); // We make an explicit call to currentTimeMillis() so that total accuracy // of lifetime calculations is better than one second. ageNode.timestamp = System.currentTimeMillis(); cacheObject.ageListNode = ageNode; // If cache is too full, remove least used cache entries until it is // not too full. cullCache(); } /** * Gets an object from cache. This method will return null under two * conditions:<ul> * <li>The object referenced by the key was never added to cache. * <li>The object referenced by the key has expired from cache.</ul> * * @param key the unique key of the object to get. * @return the Cacheable object corresponding to unique key. */ public synchronized Cacheable get(Object key) { // First, clear all entries that have been in cache longer than the // maximum defined age. deleteExpiredEntries(); CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key); if (cacheObject == null) { // The object didn't exist in cache, so increment cache misses. cacheMisses++; return null; } // The object exists in cache, so increment cache hits. cacheHits++; // Remove the object from it's current place in the cache order list, // and re-insert it at the front of the list. cacheObject.lastAccessedListNode.remove(); lastAccessedList.addFirst(cacheObject.lastAccessedListNode); return cacheObject.object; } /** * Removes an object from cache. * * @param key the unique key of the object to remove. */ public synchronized void remove(Object key) { CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key); // If the object is not in cache, stop trying to remove it. if (cacheObject == null) { return; } // remove from the hash map cachedObjectsHash.remove(key); // remove from the cache order list cacheObject.lastAccessedListNode.remove(); cacheObject.ageListNode.remove(); // remove references to linked list nodes cacheObject.ageListNode = null; cacheObject.lastAccessedListNode = null; // removed the object, so subtract its size from the total. size -= cacheObject.size; } /** * Clears the cache of all objects. The size of the cache is reset to 0. */ public synchronized void clear() { Object [] keys = cachedObjectsHash.keySet().toArray(); for (int i=0; i<keys.length; i++) { remove(keys[i]); } // Now, reset all containers. cachedObjectsHash.clear(); cachedObjectsHash = new HashMap(103); lastAccessedList.clear(); lastAccessedList = new LinkedList(); ageList.clear(); ageList = new LinkedList(); size = 0; cacheHits = 0; cacheMisses = 0; } /** * Returns an array of the keys contained in the cache. * * @return an array of the keys present in the cache. */ public Object [] keys() { return cachedObjectsHash.keySet().toArray(); } /** * Returns an array of the values contained in the cache. * * @return a array of the cache entries. */ public Object [] values() { Object [] cacheObjects = cachedObjectsHash.values().toArray(); Object [] values = new Object[cacheObjects.length]; for (int i=0; i<cacheObjects.length; i++) { values[i] = ((CacheObject)cacheObjects[i]).object; } return values; } /** * Returns the number of cache hits. A cache hit occurs every * time the get method is called and the cache contains the requested * object.<p> * * Keeping track of cache hits and misses lets one measure how efficient * the cache is; the higher the percentage of hits, the more efficient. * * @return the number of cache hits. */ public long getCacheHits() { return cacheHits; } /** * Returns the number of cache misses. A cache miss occurs every * time the get method is called and the cache does not contain the * requested object.<p> * * Keeping track of cache hits and misses lets one measure how efficient * the cache is; the higher the percentage of hits, the more efficient. * * @return the number of cache hits. */ public long getCacheMisses() { return cacheMisses; } /** * Clears all entries out of cache where the entries are older than the * maximum defined age. */ private final void deleteExpiredEntries() { //Check if expiration is turned on. if (maxLifetime <= 0) { return; } // Remove all old entries. To do this, we remove objects from the end // of the linked list until they are no longer too old. We get to avoid // any hash lookups or looking at any more objects than is strictly // neccessary. LinkedListNode node = ageList.getLast(); //If there are no entries in the age list, return. if (node == null) { return; } // Determine the expireTime, which is the moment in time that elements // should expire from cache. Then, we can do an easy to check to see // if the expire time is greater than the expire time. long expireTime = currentTime - maxLifetime; while(expireTime > node.timestamp) { // Remove the object remove(node.object); // Get the next node. node = ageList.getLast(); // If there are no more entries in the age list, return. if (node == null) { return; } } } /** * Removes objects from cache if the cache is too full. "Too full" is * defined as within 3% of the maximum cache size. Whenever the cache is * is too big, the least frequently used elements are deleted until the * cache is at least 10% empty. */ private final void cullCache() { // See if the cache size is within 3% of being too big. If so, clean out // cache until it's 10% free. if (size >= maxSize * .97) { // First, delete any old entries to see how much memory that frees. deleteExpiredEntries(); int desiredSize = (int)(maxSize * .90); while (size > desiredSize) { // Get the key and invoke the remove method on it. remove(lastAccessedList.getLast().object); } } } }
#################################################################################################################
2.LinkedList的addFirst方法
public LinkedListNode addFirst(LinkedListNode node) { node.next = head.next; node.previous = head; node.previous.next = node; node.next.previous = node; return node; }
3.LinkedListNode的remove方法
public void remove() { previous.next = next; next.previous = previous; }
#########################################################################################
数据层的cache:
4.DatabaseCacheManager
/** * $RCSfile: DatabaseCacheManager.java,v $ * $Revision: 1.7 $ * $Date: 2001/09/24 02:54:33 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ package com.jivesoftware.forum.database; import com.jivesoftware.util.*; import com.jivesoftware.forum.*; /** * Central cache management of all caches used by Jive. Cache sizes are stored * as the following Jive following Jive property values: <ul> * * <li> <tt>cache.userCache.size</tt> * <li> <tt>cache.groupCache.size</tt> * <li> <tt>cache.forumCache.size</tt> * <li> <tt>cache.threadCache.size</tt> * <li> <tt>cache.messageCache.size</tt> </ul> * * All values should be in bytes. Cache can also be globally enabled or disabled. * This value is stored as the <tt>cache.enabled</tt> Jive property. */ public class DatabaseCacheManager { public static final int USER_CACHE = 0; public static final int GROUP_CACHE = 1; public static final int FORUM_CACHE = 2; public static final int THREAD_CACHE = 3; public static final int MESSAGE_CACHE = 4; public static final int USER_PERMS_CACHE = 5; public UserCache userCache; public GroupCache groupCache; public ForumCache forumCache; public ForumThreadCache threadCache; public ForumMessageCache messageCache; public UserPermissionsCache userPermsCache; private boolean cacheEnabled = true; private DbForumFactory factory; /** * Creates a new cache manager. */ public DatabaseCacheManager(DbForumFactory factory) { this.factory = factory; // See if cache is supposed to be enabled. String enabled = JiveGlobals.getJiveProperty("cache.enabled"); if (enabled != null) { try { cacheEnabled = Boolean.valueOf(enabled).booleanValue(); } catch (Exception e) { } } // Default cache sizes (small) int forumCacheSize = 512*1024; // 1/2 MB int threadCacheSize = 512*1024; // 1/2 MB int messageCacheSize = 1152*1024; // 5/4 MB int userCacheSize = 512*1024; // 1/2 MB int userPermCacheSize = 256*1024; // 1/4 MB int groupCacheSize = 128*1024; // 1/8 MB //Now, see if properties were set. String fCacheSize = JiveGlobals.getJiveProperty("cache.forumCache.size"); if (fCacheSize != null) { try { forumCacheSize = Integer.parseInt(fCacheSize); } catch (Exception e) { } } String tCacheSize = JiveGlobals.getJiveProperty("cache.threadCache.size"); if (tCacheSize != null) { try { threadCacheSize = Integer.parseInt(tCacheSize); } catch (Exception e) { } } String mCacheSize = JiveGlobals.getJiveProperty("cache.messageCache.size"); if (mCacheSize != null) { try { messageCacheSize = Integer.parseInt(mCacheSize); } catch (Exception e) { } } String uCacheSize = JiveGlobals.getJiveProperty("cache.userCache.size"); if (uCacheSize != null) { try { userCacheSize = Integer.parseInt(uCacheSize); } catch (Exception e) { } } String upCacheSize = JiveGlobals.getJiveProperty("cache.userPermCache.size"); if (upCacheSize != null) { try { userPermCacheSize = Integer.parseInt(upCacheSize); } catch (Exception e) { } } String gCacheSize = JiveGlobals.getJiveProperty("cache.groupCache.size"); if (gCacheSize != null) { try { groupCacheSize = Integer.parseInt(gCacheSize); } catch (Exception e) { } } int MINUTE = 1000*60; int HOUR = MINUTE*60; //Initialize all cache structures 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); //The user permissions cache is a special one. It's actually a Cache //of Cache objects. Each of the cache objects in the main cache //corresponds to a particular forum, and is used to cache the //permissions that a user has for a forum. In order to handle this //requirement, we use a special subclass of Cache. userPermsCache = new UserPermissionsCache( new UserPermsCache(userPermCacheSize, 24*HOUR), factory ); } public void clear(int cacheType) { getCache(cacheType).clear(); } public long getHits(int cacheType) { return getCache(cacheType).getCacheHits(); } public long getMisses(int cacheType) { return getCache(cacheType).getCacheMisses(); } public int getSize(int cacheType) { return getCache(cacheType).getSize(); } public int getMaxSize(int cacheType) { return getCache(cacheType).getMaxSize(); } public void setMaxSize(int cacheType, int size) { getCache(cacheType).setMaxSize(size); // Save the size of the cache as a jive property switch (cacheType) { case FORUM_CACHE: JiveGlobals.setJiveProperty("cache.forumCache.size",String.valueOf(size)); break; case THREAD_CACHE: JiveGlobals.setJiveProperty("cache.threadCache.size",String.valueOf(size)); break; case MESSAGE_CACHE: JiveGlobals.setJiveProperty("cache.messageCache.size",String.valueOf(size)); break; case USER_CACHE: JiveGlobals.setJiveProperty("cache.userCache.size",String.valueOf(size)); break; case USER_PERMS_CACHE: JiveGlobals.setJiveProperty("cache.userPermsCache.size",String.valueOf(size)); break; case GROUP_CACHE: JiveGlobals.setJiveProperty("cache.groupCache.size",String.valueOf(size)); break; default: throw new IllegalArgumentException("Invalid cache type: " + cacheType); } } public int getNumElements(int cacheType) { return getCache(cacheType).getNumElements(); } private DatabaseCache getCache(int cacheType) { switch (cacheType) { case FORUM_CACHE: return forumCache; case THREAD_CACHE: return threadCache; case MESSAGE_CACHE: return messageCache; case USER_CACHE: return userCache; case USER_PERMS_CACHE: return userPermsCache; case GROUP_CACHE: return groupCache; default: throw new IllegalArgumentException("Invalid cache type: " + cacheType); } } public boolean isCacheEnabled() { return cacheEnabled; } public void setCacheEnabled(boolean cacheEnabled) { // If we're setting cacheEnabled to false, clear all caches if (cacheEnabled == false) { // Iterate through each of the five caches. for (int i=USER_CACHE; i<USER_PERMS_CACHE; i++) { clear(i); } } this.cacheEnabled = cacheEnabled; JiveGlobals.setJiveProperty("cache.enabled", String.valueOf(cacheEnabled)); } }
###############################################################################################################
5.DatabaseCache
/** * $RCSfile: DatabaseCache.java,v $ * $Revision: 1.3 $ * $Date: 2001/07/31 05:38:47 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ package com.jivesoftware.forum.database; import com.jivesoftware.util.LongCache; /** * A base class that defines the basic functionality needed to wrap the general * purpose CoolServlets cache classes to caches for specific Jive database * objects. * * @see LongCache */ public class DatabaseCache { protected LongCache cache; protected DbForumFactory factory; /** * Creates a new database cache object. * * @param cache a cache object to wrap. * @param factory a DbForumFactory to be used to perform Jive operations. */ public DatabaseCache(LongCache cache, DbForumFactory factory) { this.cache = cache; this.factory = factory; } /** * Pass-thru method for LongCache.remove(long). * * @see LongCache#remove(long) */ public void remove(long key) { cache.remove(key); } /** * Pass-thru method for LongCache.getCacheHits(). * * @see LongCache#getCacheHits() */ public long getCacheHits() { return cache.getCacheHits(); } /** * Pass-thru for LongCache.getCacheMisses(). * * @see LongCache#getCacheMisses() */ public long getCacheMisses() { return cache.getCacheMisses(); } public int getSize() { return cache.getSize(); } public void setMaxSize(int maxSize) { cache.setMaxSize(maxSize); } public int getMaxSize() { return cache.getMaxSize(); } public int getNumElements() { return cache.getNumElements(); } public void clear() { cache.clear(); } public boolean isEnabled() { return factory.cacheManager.isCacheEnabled(); } }
##############################################################################################################
6.ForumCache---先从缓存里查找,缓存里没有再从数据库中获取,再添入缓存
/** * $RCSfile: ForumCache.java,v $ * $Revision: 1.4 $ * $Date: 2001/09/18 16:40:03 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ package com.jivesoftware.forum.database; import com.jivesoftware.util.*; import com.jivesoftware.forum.util.*; import com.jivesoftware.forum.*; /** * Cache for DbForum objects. */ public class ForumCache extends DatabaseCache { protected Cache forumIDCache = new Cache(128*1024, 6*JiveGlobals.HOUR); public ForumCache(LongCache cache, DbForumFactory forumFactory) { super(cache, forumFactory); } public DbForum get(long forumID) throws ForumNotFoundException { if (!isEnabled()) { return new DbForum(forumID, factory); } // Cache is enabled. DbForum forum = (DbForum)cache.get(forumID); if (forum == null) { forum = new DbForum(forumID, factory); cache.add(forumID, forum); // -- CACHE WARMUP DISABLED. SEEMS TO DO MORE HARM THAN GOOD. -- // // Finally, schedule a warmup of the cache for the forum. //TaskEngine.addTask(new ForumCacheWarmupTask(forum), // Thread.NORM_PRIORITY); } return forum; } public Forum get(String name) throws ForumNotFoundException { // If cache is not enabled, do a new lookup of object if (!isEnabled()) { long forumID = factory.getForumID(name); return new DbForum(forumID, factory); } // Cache is enabled. CacheableLong forumIDLong = (CacheableLong)forumIDCache.get(name); // If id wan't found in cache, load it up and put it there. if (forumIDLong == null) { long forumID = factory.getForumID(name); forumIDLong = new CacheableLong(forumID); forumIDCache.add(name, forumIDLong); } return get(forumIDLong.getLong()); } public void remove(long key) { DbForum forum = (DbForum)cache.get(key); if (forum == null) { return; } // Find the name of the forum and remove it from the cache. try { String name = forum.getName(); forumIDCache.remove(name); } catch (Exception e) { e.printStackTrace(); } forum.reset(); // Now, expire relevant objects out of the factory. factory.popularForums = null; factory.popularThreads = null; } }
#####################################################################################################################
7.DBForum
/** * $RCSfile: DbForum.java,v $ * $Revision: 1.25 $ * $Date: 2001/10/12 23:02:15 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ package com.jivesoftware.forum.database; import java.util.*; import java.util.Date; import java.sql.*; import java.io.*; import com.jivesoftware.forum.*; import com.jivesoftware.util.*; import com.jivesoftware.forum.gateway.*; /** * Database implementation of the Forum interface. It loads and stores forum * information from a a database. * * @see Forum */ public class DbForum implements Forum, Cacheable { /** DATABASE QUERIES **/ private static final String ALL_THREADS = "SELECT threadID from jiveThread WHERE forumID=?"; private static final String ADD_THREAD = "UPDATE jiveThread set forumID=? WHERE threadID=?"; private static final String MOVE_MESSAGES = "UPDATE jiveMessage set forumID=? WHERE threadID=?"; protected static final String DELETE_THREAD = "DELETE FROM jiveThread WHERE threadID=?"; private static final String DELETE_THREAD_PROPERTIES = "DELETE FROM jiveThreadProp WHERE threadID=?"; private static final String LOAD_PROPERTIES = "SELECT name, propValue FROM jiveForumProp WHERE forumID=?"; private static final String DELETE_PROPERTY = "DELETE FROM jiveForumProp WHERE forumID=? AND name=?"; private static final String DELETE_PROPERTIES = "DELETE FROM jiveForumProp WHERE forumID=?"; private static final String INSERT_PROPERTY = "INSERT INTO jiveForumProp(forumID,name,propValue) VALUES(?,?,?)"; private static final String LOAD_FORUM = "SELECT forumID, name, description, modDefaultThreadVal, " + "modDefaultMsgVal, modMinThreadVal, modMinMsgVal, modifiedDate, " + "creationDate FROM jiveForum WHERE forumID=?"; private static final String ADD_FORUM = "INSERT INTO jiveForum(forumID, name, description, modDefaultThreadVal, " + "modDefaultMsgVal, modMinThreadVal, modMinMsgVal, modifiedDate, creationDate)" + " VALUES (?,?,?,?,?,?,?,?,?)"; private static final String SAVE_FORUM = "UPDATE jiveForum SET name=?, description=?, modDefaultThreadVal=?, " + "modDefaultMsgVal=?, modMinThreadVal=?, modMinMsgVal=?, " + "modifiedDate=?, creationDate=? WHERE forumID=?"; private static final String UPDATE_FORUM_MODIFIED_DATE = "UPDATE jiveForum SET modifiedDate=? WHERE forumID=?"; private static final String POPULAR_THREADS = "SELECT threadID, count(1) AS msgCount FROM jiveMessage WHERE " + "modifiedDate > ? AND forumID=? GROUP BY threadID ORDER BY msgCount DESC"; private static final String POPULAR_THREADS_ORACLE = "SELECT /*+ INDEX (jiveMessage jiveMessage_mDate_idx) */ threadID, " + "count(1) AS msgCount FROM jiveMessage WHERE modifiedDate > ? " + "AND forumID=? GROUP BY threadID ORDER BY msgCount DESC"; /* // Note, the above query includes hints for Oracle, which are necessary // so that modified date index will always be used. This is a great // tradeoff when the time window you're looking at is not excessively // large. MySQL also handles the query very quickly. If your own db // needs a hint, you may want to edit the sql logic to add it. */ /** * Controls whether extended properties should be lazily loaded (not loaded * until requested). If the properties are infrequently used, this provides * a great speedup in initial object loading time. However, if your * application does use extended properties all the time, you may wish to * turn lazy loading off, as it's actually faster in total db lookup time * to load everything at once. */ private static final boolean LAZY_PROP_LOADING = true; /** * Number of threadID's per cache block. */ public static final int THREAD_BLOCK_SIZE = 200; /** * Number of messageID's per cache block. */ public static final int MESSAGE_BLOCK_SIZE = 100; // Constant for an empty bock. This is returned in the case that there are // no results when trying to load a thread or message block. private static final long[] EMPTY_BLOCK = new long[0]; // A ResultFilter is used to filter and sort the values that are returned // from the threads() and messages() methods. We use a default private static final ResultFilter DEFAULT_THREAD_FILTER = ResultFilter.createDefaultThreadFilter(); private static final ResultFilter DEFAULT_MESSAGE_FILTER = ResultFilter.createDefaultMessageFilter(); /** * Cache for lists of thread id's. The default size is 16K, which should * let us hold about 2000 thread id's in memory at once. If you have a lot * of memory and very large forums, you may wish to make the size of this * cache considerably larger. */ protected Cache threadListCache = new Cache(16*1024, JiveGlobals.HOUR * 6); /** * Cache for lists of message id's. The default size is 8K, which should * let us hold about 100 message id's in memory at once. If you have a lot * of memory and very large forums, you may wish to make the size of this * cache considerably larger. */ protected Cache messageListCache = new Cache(8*1024, JiveGlobals.HOUR * 6); /** * Cache for thread counts. Default size is 256 bytes. */ protected Cache threadCountCache = new Cache(256, JiveGlobals.HOUR * 6); /** * Cache for message counts. Default size is 256 bytes. */ protected Cache messageCountCache = new Cache(256, JiveGlobals.HOUR * 6); private long id = -1; private String name; private String description; private java.util.Date creationDate; private java.util.Date modifiedDate; private int modDefaultThreadValue = 1; private int modDefaultMessageValue = 1; private int modMinThreadValue = 1 ; private int modMinMessageValue = 1; private Map properties; private DbForumFactory factory; private long[] popularThreads = null; private DbFilterManager filterManager; /** * Creates a new forum with the specified name and description. * * @param name the name of the forum. * @param description the description of the forum. * @param factory the DbForumFactory the forum is a part of. */ protected DbForum(String name, String description, DbForumFactory factory) { this.id = SequenceManager.nextID(JiveGlobals.FORUM); this.name = name; this.description = description; long now = System.currentTimeMillis(); creationDate = new java.util.Date(now); modifiedDate = new java.util.Date(now); this.factory = factory; insertIntoDb(); properties = new Hashtable(); init(); } public void setName(String name) throws ForumAlreadyExistsException { // If the new name is the same as the current name, do nothing. if (this.name.equals(name)) { return; } // If a forum with the new name already exists, throw an exception. try { // Check to make sure that's it's not just a case change. If we // don't do this check, the db lookup may give us a false-positive // that the name is already in use. if (!this.name.toLowerCase().equals(name.toLowerCase())) { Forum forum = factory.getForum(name); // If we get here, the forum must already exist, so throw exception. throw new ForumAlreadyExistsException(); } } catch (ForumNotFoundException e) { } // This is a special case since names of forums and id's are linked // and cached. Before changing the name of the forum to the new name, // we need to clear from cache to remove the old name to id mapping. factory.cacheManager.forumCache.remove(id); this.name = name; saveToDb(); // Finally, expire from cache again to revflect name change. factory.cacheManager.forumCache.remove(id); } public void setDescription(String description) { this.description = description; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setCreationDate(java.util.Date creationDate) { this.creationDate = creationDate; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setModifiedDate(java.util.Date modifiedDate) throws UnauthorizedException { this.modifiedDate = modifiedDate; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setModerationDefaultMessageValue(int value) { this.modDefaultMessageValue = value; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setModerationMinThreadValue(int value) { this.modMinThreadValue = value; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setModerationMinMessageValue(int value) { this.modMinMessageValue = value; saveToDb(); factory.cacheManager.forumCache.remove(id); } public void setProperty(String name, String value) { if (LAZY_PROP_LOADING) { if (properties == null) { loadPropertiesFromDb(); } } properties.put(name, value); savePropertiesToDb(); factory.cacheManager.forumCache.remove(id); } public void deleteProperty(String name) { if (LAZY_PROP_LOADING) { if (properties == null) { loadPropertiesFromDb(); } } properties.remove(name); deletePropertyFromDb(name); factory.cacheManager.forumCache.remove(id); } public void addThread(ForumThread thread) { boolean abortTransaction = false; // Add thread to forum table. Connection con = null; PreparedStatement pstmt = null; try { con = ConnectionManager.getTransactionConnection(); try { pstmt = con.prepareStatement(ADD_THREAD); pstmt.setLong(1,id); pstmt.setLong(2,thread.getID()); pstmt.executeUpdate(); } finally { try { pstmt.close(); } catch (Exception e) { e.printStackTrace(); } } // Now, insert the thread into the database. Depending on if this // method was called internally or not, the thread object might // be wrapped by a proxy or not. if (thread instanceof ForumThreadProxy) { ForumThreadProxy proxyThread = (ForumThreadProxy)thread; DbForumThread dbThread = (DbForumThread)proxyThread.getProxiedForumThread(); dbThread.insertIntoDb(this, con); } else { DbForumThread dbThread = (DbForumThread)thread; dbThread.insertIntoDb(this, con); } // Check the moderation value of the thread. If the value is above // the visible threshold for the forum, then update the modified // date of the forum. Otherwise, we'll wait to update the modified // date to when the thread is moderated to be visible. if (thread.getModerationValue() >= getModerationMinThreadValue()) { updateModifiedDate(thread.getModifiedDate().getTime(), con); } } catch(Exception e) { e.printStackTrace(); abortTransaction = true; } finally { ConnectionManager.closeTransactionConnection(con, abortTransaction); } // Thread count has changed, so remove the forum from cache. factory.cacheManager.forumCache.remove(this.id); // Expire the userMessageCountCache if the root message was not posted // anonymously. ForumMessage message = thread.getRootMessage(); if (!message.isAnonymous()) { factory.userMessageCountCache.remove(message.getUser().getID()); } } public void deleteThread(ForumThread thread) { boolean abortTransaction = false; Connection con = null; try { con = ConnectionManager.getTransactionConnection(); DbForumThread dbThread = (DbForumThread)getThread(thread.getID()); deleteThread(dbThread, con); } catch (Exception e) { e.printStackTrace(); abortTransaction = true; } finally { ConnectionManager.closeTransactionConnection(con, abortTransaction); } // Now, delete thread and forum from cache factory.cacheManager.threadCache.remove(thread.getID()); factory.cacheManager.forumCache.remove(this.id); } public void moveThread(ForumThread thread, Forum forum) throws UnauthorizedException { // Ensure that thread belongs to this forum if (thread.getForum().getID() != this.id) { throw new IllegalArgumentException("The thread does not belong to this forum."); } // Read all messageIDs of the thread into an array so that we can expire // each of them later. ResultFilter ignoreModerationFilter = ResultFilter.createDefaultMessageFilter(); ignoreModerationFilter.setModerationRangeMin(Integer.MIN_VALUE+1); LongList messageIDList = new LongList(); Iterator iter = thread.messages(ignoreModerationFilter); while (iter.hasNext()) { long messageID = ((ForumMessage)iter.next()).getID(); messageIDList.add(messageID); } // Modify the SQL record. Only the thread table has information about // forumID, so we only need to modify that record. The message records // underneath the thread can be left alone. boolean abortTransaction = false; Connection con = null; try { con = ConnectionManager.getTransactionConnection(); PreparedStatement pstmt = null; try { pstmt = con.prepareStatement(ADD_THREAD); pstmt.setLong(1,forum.getID()); pstmt.setLong(2,thread.getID()); pstmt.executeUpdate(); pstmt.close(); // Move all messages in thread to new forum. pstmt = con.prepareStatement(MOVE_MESSAGES); pstmt.setLong(1,forum.getID()); pstmt.setLong(2,thread.getID()); pstmt.executeUpdate(); } finally { try { pstmt.close(); } catch (Exception e) { e.printStackTrace(); } } } catch( SQLException sqle ) { sqle.printStackTrace(); abortTransaction = true; return; } finally { ConnectionManager.closeTransactionConnection(con, abortTransaction); } DatabaseCacheManager cacheManager = factory.cacheManager; SearchManager searchManager = factory.getSearchManager(); // Remove both forums from cache. cacheManager.forumCache.remove(this.id); cacheManager.forumCache.remove(forum.getID()); // Update the last modified date of both forums to the most recently // updated thread (this may have changed during the move thread operation). ResultFilter newestThreadFilter = ResultFilter.createDefaultThreadFilter(); newestThreadFilter.setNumResults(1); Iterator threadIter = threads(newestThreadFilter); if (threadIter.hasNext()) { ForumThread newestThread = (ForumThread)threadIter.next(); if (newestThread != null) { setModifiedDate(newestThread.getModifiedDate()); } } // Updated modified date of other forum. newestThreadFilter = ResultFilter.createDefaultThreadFilter(); newestThreadFilter.setNumResults(1); threadIter = forum.threads(newestThreadFilter); if (threadIter.hasNext()) { ForumThread newestThread = (ForumThread)threadIter.next(); if (newestThread != null) { forum.setModifiedDate(newestThread.getModifiedDate()); } } // Remove thread from cache. cacheManager.threadCache.remove(thread.getID()); // Loop through all messages in thread and delete from cache, reset // entry in the search index to new thread. long [] messageIDArray = messageIDList.toArray(); for (int i=0; i<messageIDArray.length; i++) { long messageID = messageIDArray[i]; cacheManager.messageCache.remove(messageID); try { ForumMessage message = thread.getMessage(messageID); searchManager.removeFromIndex(message); searchManager.addToIndex(message); } catch (ForumMessageNotFoundException e) { } } } public int getThreadCount(ResultFilter resultFilter) { String query = getThreadListSQL(resultFilter, true); CacheableInt count = (CacheableInt)threadCountCache.get(query); // If already in cache, return the count. if (count != null) { return count.getInt(); } // Otherwise, we have to load the count from the db. else { int threadCount = 0; Connection con = null; Statement stmt = null; try { con = ConnectionManager.getConnection(); stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); rs.next(); threadCount = rs.getInt(1); } catch( SQLException sqle ) { sqle.printStackTrace(); } finally { try { stmt.close(); } catch (Exception e) { e.printStackTrace(); } try { con.close(); } catch (Exception e) { e.printStackTrace(); } } // Add the thread count to cache threadCountCache.add(query, new CacheableInt(threadCount)); return threadCount; } } public int getMessageCount(ResultFilter resultFilter) { String query = getMessageListSQL(resultFilter, true); CacheableInt count = (CacheableInt)messageCountCache.get(query); // If already in cache, return the count. if (count != null) { return count.getInt(); } // Otherwise, we have to load the count from the db. else { int messageCount = 0; Connection con = null; Statement stmt = null; try { con = ConnectionManager.getConnection(); stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); rs.next(); messageCount = rs.getInt(1); } catch( SQLException sqle ) { sqle.printStackTrace(); } finally { try { stmt.close(); } catch (Exception e) { e.printStackTrace(); } try { con.close(); } catch (Exception e) { e.printStackTrace(); } } // Add the thread count to cache messageCountCache.add(query, new CacheableInt(messageCount)); return messageCount; } } public int hashCode() { return (int)id; } public boolean equals(Object object) { if (this == object) { return true; } if (object != null && object instanceof DbForum) { return id == ((DbForum)object).getID(); } else { return false; } } /** * Updates the modified date. It accepts a Connection so that it can * participate in trasactions. */ protected void updateModifiedDate(long date, Connection con) throws SQLException { this.modifiedDate.setTime(date); PreparedStatement pstmt = null; try { pstmt = con.prepareStatement(UPDATE_FORUM_MODIFIED_DATE); pstmt.setString(1, StringUtils.dateToMillis(modifiedDate)); pstmt.setLong(2, id); pstmt.executeUpdate(); } finally { try { pstmt.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * Does the actual work of deleteting a thread. It accepts a Connection so * that it can participate in transactions. */ protected void deleteThread(DbForumThread thread, Connection con) throws SQLException, ForumMessageNotFoundException { // Delete all messages from the thread. Deleting the root // message will delete all submessages. thread.deleteMessage(thread.getRootMessage(), con); // Delete any watches on the thread. factory.watchManager.deleteThreadWatches(thread, con); // Now delete thread db entry and thread properties. PreparedStatement pstmt = null; try { pstmt = con.prepareStatement(DELETE_THREAD_PROPERTIES); pstmt.setLong(1,thread.getID()); pstmt.execute(); pstmt.close(); pstmt = con.prepareStatement(DELETE_THREAD); pstmt.setLong(1,thread.getID()); pstmt.execute(); } finally { try { pstmt.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * Returns a block of threadID's from a query and performs transparent * caching of those blocks. The two parameters specify a database query * and a startIndex for the results in that query. * * @param query the SQL thread list query to cache blocks from. * @param startIndex the startIndex in the list to get a block for. */ protected long[] getThreadBlock(String query, int startIndex) { // First, discover what block number the results will be in. int blockID = startIndex / THREAD_BLOCK_SIZE; int blockStart = blockID * THREAD_BLOCK_SIZE; // Now, check cache to see if the block is already cached. The key is // simply the query plus the blockID. String key = query + blockID; CacheableLongArray longArray = (CacheableLongArray)threadListCache.get(key); //If already in cache, return the block. if (longArray != null) { /** * The actual block may be smaller than THREAD_BLOCK_SIZE. If that's * the case, it means two things: * 1) We're at the end boundary of all the results. * 2) If the start index is greater than the length of the current * block, than there aren't really any results to return. */ long [] threads = longArray.getLongArray(); if (startIndex >= blockStart + threads.length) { // Return an empty array return EMPTY_BLOCK; } else { return threads; } } // Otherwise, we have to load up the block from the database. else { LongList threadsList = new LongList(THREAD_BLOCK_SIZE); Connection con = null; Statement stmt = null; try { con = ConnectionManager.getConnection(); stmt = con.createStatement(); // Set the maxium number of rows to end at the end of this block. ConnectionManager.setMaxRows(stmt, THREAD_BLOCK_SIZE * (blockID+1)); ResultSet rs = stmt.executeQuery(query); // Grab THREAD_BLOCK_ROWS rows at a time. ConnectionManager.setFetchSize(rs, THREAD_BLOCK_SIZE); // Many JDBC drivers don't implement scrollable cursors the real // way, but instead load all results into memory. Looping through // the results ourselves is more efficient. for (int i=0; i<blockStart; i++) { rs.next(); } // Keep reading results until the result set is exaughsted or // we come to the end of the block. int count = 0; while (rs.next() && count < THREAD_BLOCK_SIZE) { threadsList.add(rs.getLong(1)); count++; } } catch( SQLException sqle ) { sqle.printStackTrace(); } finally { try { stmt.close(); } catch (Exception e) { e.printStackTrace(); } try { con.close(); } catch (Exception e) { e.printStackTrace(); } } long [] threads = threadsList.toArray(); // Add the thread block to cache threadListCache.add(key, new CacheableLongArray(threads)); /** * The actual block may be smaller than THREAD_BLOCK_SIZE. If that's * the case, it means two things: * 1) We're at the end boundary of all the results. * 2) If the start index is greater than the length of the current * block, than there aren't really any results to return. */ if (startIndex >= blockStart + threads.length) { // Return an empty array return EMPTY_BLOCK; } else { return threads; } } } /** * Returns a block of messageID's from a query and performs transparent * caching of those blocks. The two parameters specify a database query * and a startIndex for the results in that query. * * @param query the SQL message list query to cache blocks from. * @param startIndex the startIndex in the list to get a block for. */ protected long[] getMessageBlock(String query, int startIndex) { // First, discover what block number the results will be in. int blockID = startIndex / MESSAGE_BLOCK_SIZE; int blockStart = blockID * MESSAGE_BLOCK_SIZE; // Now, check cache to see if the block is already cached. The key is // simply the query plus the blockID. String key = query + blockID; CacheableLongArray longArray = (CacheableLongArray)messageListCache.get(key); // If already in cache, return the block. if (longArray != null) { /** * The actual block may be smaller than MESSAGE_BLOCK_SIZE. If that's * the case, it means two things: * 1) We're at the end boundary of all the results. * 2) If the start index is greater than the length of the current * block, than there aren't really any results to return. */ long [] messages = longArray.getLongArray(); if (startIndex >= blockStart + messages.length) { // Return an empty array return EMPTY_BLOCK; } else { return messages; } } // Otherwise, we have to load up the block from the database. else { LongList messagesList = new LongList(MESSAGE_BLOCK_SIZE); Connection con = null; Statement stmt = null; try { con = ConnectionManager.getConnection(); stmt = con.createStatement(); // Set the maxium number of rows to end at the end of this block. ConnectionManager.setMaxRows(stmt, MESSAGE_BLOCK_SIZE * (blockID+1)); ResultSet rs = stmt.executeQuery(query); // Grab MESSAGE_BLOCK_ROWS rows at a time. ConnectionManager.setFetchSize(rs, MESSAGE_BLOCK_SIZE); // Many JDBC drivers don't implement scrollable cursors the real // way, but instead load all results into memory. Looping through // the results ourselves is more efficient. for (int i=0; i<blockStart; i++) { rs.next(); } // Keep reading results until the result set is exaughsted or // we come to the end of the block. int count = 0; while (rs.next() && count < MESSAGE_BLOCK_SIZE) { messagesList.add(rs.getLong(1)); count++; } } catch( SQLException sqle ) { sqle.printStackTrace(); } finally { try { stmt.close(); } catch (Exception e) { e.printStackTrace(); } try { con.close(); } catch (Exception e) { e.printStackTrace(); } } long [] messages = messagesList.toArray(); // Add the message block to cache messageListCache.add(key, new CacheableLongArray(messages)); /** * The actual block may be smaller than MESSAGE_BLOCK_SIZE. If that's * the case, it means two things: * 1) We're at the end boundary of all the results. * 2) If the start index is greater than the length of the current * block, than there aren't really any results to return. */ if (startIndex >= blockStart + messages.length) { //Return an empty array return EMPTY_BLOCK; } else { return messages; } } } }
维护缓存与原对象的一致性
/** * $RCSfile: DbForumThread.java,v $ * $Revision: 1.23 $ * $Date: 2001/09/23 18:54:08 $ * * Copyright (C) 1999-2001 CoolServlets, Inc. All rights reserved. * * This software is the proprietary information of CoolServlets, Inc. * Use is subject to license terms. */ public void addMessage(ForumMessage parentMessage, ForumMessage newMessage) throws UnauthorizedException { // Get the underlying DbForumMessage object. DbForumMessage dbMessage = null; if (newMessage instanceof ForumMessageProxy) { ForumMessageProxy proxyMessage = (ForumMessageProxy)newMessage; dbMessage = (DbForumMessage)proxyMessage.getProxiedForumMessage(); } else { dbMessage = (DbForumMessage)newMessage; } DbForum dbForum = null; boolean abortTransaction = false; Connection con = null; try { con = ConnectionManager.getTransactionConnection(); // Insert the message into the database. dbMessage.insertIntoDb(this, parentMessage.getID(), con); // Check the moderation value of the message. If the value is above // the visible threshold for the forum, then update the modified // date of the thread and forum. Otherwise, we'll wait to update // the modified dates to when the message is moderated to be visible. dbForum = factory.cacheManager.forumCache.get(forumID); if (newMessage.getModerationValue() >= dbForum.getModerationMinMessageValue()) { long modifiedDate = newMessage.getModifiedDate().getTime(); updateModifiedDate(modifiedDate, con); dbForum.updateModifiedDate(modifiedDate, con); } } catch( Exception e ) { e.printStackTrace(); abortTransaction = true; } finally { ConnectionManager.closeTransactionConnection(con, abortTransaction); } // Thread count has been modified, remove thread and forum from cache. factory.cacheManager.threadCache.remove(this.id); factory.cacheManager.forumCache.remove(this.forumID); // Expire the userMessageCountCache if this message was not posted // anonymously. if (!newMessage.isAnonymous()) { factory.userMessageCountCache.remove(newMessage.getUser().getID()); } // If above the moderation threshold... if (newMessage.getModerationValue() >= dbForum.getModerationMinMessageValue()) { // Notify the watch manager that the thread has been updated. factory.watchManager.notifyWatches(this); // Notify the gateway manager of a new message. dbForum.getGatewayManager().exportData(dbMessage); } } }