Wednesday, June 17, 2009

JCS is intended to speed up applications by providing a means to manage cached data of various dynamic natures. Like any caching system, JCS is most useful for high read, low put applications. Latency times drop sharply and bottlenecks move away from the database in an effectively cached system.

The JCS goes beyond simply caching objects in memory. It provides numerous additional features:

  • Memory management
  • Disk overflow (and defragmentation)
  • Thread pool controls
  • Element grouping
  • Minimal dependencies
  • Quick nested categorical removal
  • Data expiration (idle time and max life)
  • Extensible framework
  • Fully configurable runtime parameters
  • Region data separation and configuration
  • Fine grained element configuration options
  • Remote synchronization
  • Remote store recovery
  • Non-blocking "zombie" (balking facade) pattern
  • Lateral distribution of elements via HTTP, TCP, or UDP
  • UDP Discovery of other caches
  • Element event handling
  • Remote server chaining (or clustering) and failover
  • Custom event logging hooks
  • Custom event queue injection
  • Custom object serializer injection
  • Key pattern matching retrieval
  • Network efficient multi-key retrieval

JCS works on JDK versions 1.4 and up. It only has two dependencies: Commons Logging and Doug Lea's Util Concurrent.

For configuration details you can visit here.

Example

We could create a value object for this table called BookVObj that has variables with the same names as the table columns that might look like this:

package com.genericbookstore.data;

import java.io.Serializable;
import java.util.Date;

public class BookVObj implements Serializable
{
public int bookId = 0;
public String title;
public String author;
public String ISBN;
public String price;
public Date publishDate;

public BookVObj()
{
}
}
Then we can create a manager called BookVObjManager to store and retrieve BookVObj's. All access to core book data should go through this class, including inserts and updates, to keep the caching simple. Let's make BookVObjManager a singleton that gets a JCS access object in initialization. The start of the class might look like:

package com.genericbookstore.data;

import org.apache.jcs.JCS;
// in case we want to set some special behavior
import org.apache.jcs.engine.behavior.IElementAttributes;

public class BookVObjManager
{
private static BookVObjManager instance;
private static int checkedOut = 0;
private static JCS bookCache;

private BookVObjManager()
{
try
{
bookCache = JCS.getInstance("bookCache");
}
catch (Exception e)
{
// Handle cache region initialization failure
}

// Do other initialization that may be necessary, such as getting
// references to any data access classes we may need to populate
// value objects later
}

/**
* Singleton access point to the manager.
*/
public static BookVObjManager getInstance()
{
synchronized (BookVObjManager.class)
{
if (instance == null)
{
instance = new BookVObjManager();
}
}

synchronized (instance)
{
instance.checkedOut++;
}

return instance;
}
To get a BookVObj we will need some access methods in the manager. We should be able to get a non-cached version if necessary, say before allowing an administrator to edit the book data. The methods might look like:

/**
* Retrieves a BookVObj. Default to look in the cache.
*/
public BookVObj getBookVObj(int id)
{
return getBookVObj(id, true);
}

/**
* Retrieves a BookVObj. Second argument decides whether to look
* in the cache. Returns a new value object if one can't be
* loaded from the database. Database cache synchronization is
* handled by removing cache elements upon modification.
*/
public BookVObj getBookVObj(int id, boolean fromCache)
{
BookVObj vObj = null;

// First, if requested, attempt to load from cache

if (fromCache)
{
vObj = (BookVObj) bookCache.get("BookVObj" + id);
}

// Either fromCache was false or the object was not found, so
// call loadBookVObj to create it

if (vObj == null)
{
vObj = loadvObj(id);
}

return vObj;
}

/**
* Creates a BookVObj based on the id of the BOOK table. Data
* access could be direct JDBC, some or mapping tool, or an EJB.
*/
public BookVObj loadBookVObj(int id)
{
BookVObj vObj = new BookVObj();

vObj.bookID = id;

try
{
boolean found = false;

// load the data and set the rest of the fields
// set found to true if it was found

found = true;

// cache the value object if found

if (found)
{
// could use the defaults like this
// bookCache.put( "BookVObj" + id, vObj );
// or specify special characteristics

// put to cache

bookCache.put("BookVObj" + id, vObj);
}

}
catch (Exception e)
{
// Handle failure putting object to cache
}

return vObj;
}
We will also need a method to insert and update book data. To keep the caching in one place, this should be the primary way core book data is created. The method might look like:

    /**
* Stores BookVObj's in database. Clears old items and caches
* new.
*/
public int storeBookVObj(BookVObj vObj)
{
try
{
// since any cached data is no longer valid, we should
// remove the item from the cache if it an update.

if (vObj.bookID != 0)
{
bookCache.remove("BookVObj" + vObj.bookID);
}

// put the new object in the cache

bookCache.put("BookVObj" + id, vObj);
}
catch (Exception e)
{
// Handle failure removing object or putting object to cache.
}
}
}

The first step in creating a cache region is to determine the makeup of the memory cache. For the book store example, I would create a region that could store a bit over the minimum number I want to have in memory, so the core items always readily available. I would set the maximum memory size to 1200. In addition, I might want to have all objects in this cache region expire after 7200 seconds. This can be configured in the element attributes on a default or per-region basis as illustrated in the configuration file below.

For most cache regions you will want to use a disk cache if the data takes over about .5 milliseconds to create. The indexed disk cache is the most efficient disk caching auxiliary, and for normal usage it is recommended.

The next step will be to select an appropriate distribution layer. If you have a back-end server running an appserver or scripts or are running multiple webserver VMs on one machine, you might want to use the centralized remote cache. The lateral cache would be fine, but since the lateral cache binds to a port, you'd have to configure each VM's lateral cache to listen to a different port on that machine.

If your environment is very flat, say a few load-balanced webservers and a database machine or one webserver with multiple VMs and a database machine, then the lateral cache will probably make more sense. The TCP lateral cache is recommended.

For the book store configuration I will set up a region for the bookCache that uses the LRU memory cache, the indexed disk auxiliary cache, and the remote cache. The configuration file might look like this:

# DEFAULT CACHE REGION

# sets the default aux value for any non configured caches
jcs.default=DC,RFailover
jcs.default.cacheattributes=
org.apache.jcs.engine.CompositeCacheAttributes
jcs.default.cacheattributes.MaxObjects=1000
jcs.default.cacheattributes.MemoryCacheName=
org.apache.jcs.engine.memory.lru.LRUMemoryCache
jcs.default.elementattributes.IsEternal=false
jcs.default.elementattributes.MaxLifeSeconds=3600
jcs.default.elementattributes.IdleTime=1800
jcs.default.elementattributes.IsSpool=true
jcs.default.elementattributes.IsRemote=true
jcs.default.elementattributes.IsLateral=true


# CACHE REGIONS AVAILABLE

# Regions preconfigured for caching
jcs.region.bookCache=DC,RFailover
jcs.region.bookCache.cacheattributes=
org.apache.jcs.engine.CompositeCacheAttributes
jcs.region.bookCache.cacheattributes.MaxObjects=1200
jcs.region.bookCache.cacheattributes.MemoryCacheName=
org.apache.jcs.engine.memory.lru.LRUMemoryCache
jcs.region.bookCache.elementattributes.IsEternal=false
jcs.region.bookCache.elementattributes.MaxLifeSeconds=7200
jcs.region.bookCache.elementattributes.IdleTime=1800
jcs.region.bookCache.elementattributes.IsSpool=true
jcs.region.bookCache.elementattributes.IsRemote=true
jcs.region.bookCache.elementattributes.IsLateral=true

# AUXILIARY CACHES AVAILABLE

# Primary Disk Cache -- faster than the rest because of memory key storage
jcs.auxiliary.DC=
org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheFactory
jcs.auxiliary.DC.attributes=
org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes
jcs.auxiliary.DC.attributes.DiskPath=/usr/opt/bookstore/raf
jcs.auxiliary.DC.attributes.MaxPurgatorySize=10000
jcs.auxiliary.DC.attributes.MaxKeySize=10000
jcs.auxiliary.DC.attributes.OptimizeAtRemoveCount=300000
jcs.auxiliary.DC.attributes.MaxRecycleBinSize=7500

# Remote RMI Cache set up to failover
jcs.auxiliary.RFailover=
org.apache.jcs.auxiliary.remote.RemoteCacheFactory
jcs.auxiliary.RFailover.attributes=
org.apache.jcs.auxiliary.remote.RemoteCacheAttributes
jcs.auxiliary.RFailover.attributes.RemoteTypeName=LOCAL
jcs.auxiliary.RFailover.attributes.FailoverServers=scriptserver:1102
jcs.auxiliary.RFailover.attributes.GetOnly=false