Ehcache is a popular caching library in the Java world. So far, I was aware only of the Ehcache library, using which you could build in-JVM caches. Lately (since late 2008 actually), they have come up with the Ehcache server, with which you can create remote caches that your application can access over HTTP.
This obviously has enormous implications for scalability. Granted, cache access times over a network are much higher than in-memory access, I think this is a fair tradeoff to make when you are dealing with potentially very large caches. Keeping your cache behind a server frees up your application JVM's memory. Also, if you want more cache, you can partition it out into more servers.
Our first cut of the ehcache setup was something like this. A bunch of applications would maintain their own in-JVM caches, but these caches would communicate with each other and replicate over RMI, as shown in the diagram below. This was mainly to test out the local replicated mode, which we used in our final setup for fault tolerance (see below).
The next step was to take the cache and put it behind the cache server. We used Ehcache Standalone Server version 0.8. It comes packaged within a Glassfish Application Server, so all we had to do was to expand the tarball, and update the ehcache.xml file in the war/WEB-INF/classes subdirectory with our local replicated cache definitions. To start the Ehcache server, use bin/startup.sh (this works fine, but needs cleaning up to log to a file, etc). Out of the box, its set to have the cache server listen on port 8080, you can change it to whatever you want instead.
Now, if we needed more cache memory, we could now add more server pairs (paired for fault tolerance, see diagram above) and partition the key space. Then on the client, we could do a simple hashmod of the key and direct it to the appropriate load balancer.
To make the transition seamless, I factored out all ehcache access code into a helper class - the application code was basically doing get(), put() and delete() calls on the cache - then switched the calls from local to remote cache. The code is shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | package com.mycompany.myapp.helpers;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import net.sf.ehcache.Element;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Service;
@Service("cacheHelper")
public class CacheHelper {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired private String serviceUrl;
@Autowired private int readTimeout;
@Autowired private int connectTimeout;
@Autowired private int maxRetries;
@Autowired private HttpClient httpClient;
public Element get(String cacheName, String key) throws Exception {
String url = StringUtils.join(new String[] {
serviceUrl, cacheName, key}, "/");
GetMethod getMethod = new GetMethod(url);
configureMethod(getMethod);
ObjectInputStream oin = null;
int status = -1;
try {
status = httpClient.executeMethod(getMethod);
if (status == HttpStatus.SC_NOT_FOUND) {
// if the content is deleted already
return null;
}
InputStream in = getMethod.getResponseBodyAsStream();
oin = new ObjectInputStream(in);
Element element = (Element) oin.readObject();
return element;
} catch (IOException e) {
logger.warn("GET Failed (" + status + ")", e);
} finally {
IOUtils.closeQuietly(oin);
getMethod.releaseConnection();
}
return null;
}
public void put(String cacheName, String key, Serializable value)
throws Exception {
Element element = new Element(key, value);
String url = StringUtils.join(new String[] {
serviceUrl, cacheName, key}, "/");
PutMethod putMethod = new PutMethod(url);
configureMethod(putMethod);
ObjectOutputStream oos = null;
int status = -1;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(element);
putMethod.setRequestEntity(new InputStreamRequestEntity(
new ByteArrayInputStream(bos.toByteArray())));
status = httpClient.executeMethod(putMethod);
} catch (Exception e) {
logger.warn("PUT Failed (" + status + ")", e);
} finally {
IOUtils.closeQuietly(oos);
putMethod.releaseConnection();
}
}
public void delete(String cacheName, String key) throws Exception {
String url = StringUtils.join(new String[] {
serviceUrl, cacheName, key}, "/");
DeleteMethod deleteMethod = new DeleteMethod(url);
configureMethod(deleteMethod);
int status = -1;
try {
status = httpClient.executeMethod(deleteMethod);
} catch (Exception e) {
logger.warn("DELETE Failed (" + status + ")", e);
} finally {
deleteMethod.releaseConnection();
}
}
private void configureMethod(HttpMethod method) {
if (readTimeout > 0) {
method.getParams().setSoTimeout(readTimeout);
}
if (maxRetries > 0) {
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(maxRetries, false));
}
}
}
|
Calling code is no different from the code calling the local cache. Instead of cache.XXX() you now do cacheHelper.XXX() calls. The service URL is /ehcache/rest.
Here is the cache definition for one of our caches. You can repeat the cache block for multiple caches. The peer listener and peer provider factory defintions are shared across multiple caches.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <ehcache>
...
<defaultCache .../>
<cache name="my-cache"
maxElementsInMemory="10000"
eternal="true"
diskPersistent="true"
overflowToDisk="true">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"/>
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="hostName=localhost,
peerDiscovery=automatic,
multicastGroupAddress=224.0.0.1,
multicastGroupPort=4446,
timeToLive=1"/>
</ehcache>
|
Obviously, neither the code or configuration is rocket science, there are enough examples in the Ehcache site for someone to build this stuff themselves. But the code examples of using the Ehcache server are quite generic, they concentrate on caching strings, while most people who have used Ehcache in local mode tend to cache real Serializable objects. Also, most enterprise type places I know tend to use Jakarta's HTTPClient rather than Java's URLConnection. Also, it took me a fair bit of time to figure out and test the distributed cache configuration. So if you are looking for a quick ramp up to using Ehcache server, then this post may be helpful.