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.
Thank you sir for such a nice article. It really helped a lot.
ReplyDeleteI have just one question:
I know that we can use JAVA serialization and through it we can store object as value but as I understand for key this might not be a good solution as we are making URL from the key.
Can you suggest me some better way by which I can use Object in the key also.
Thank rAun007. If you want to use some serializable object as key, then one way would be to create some sort of hash of its fields (MD5 or SHA1 or homegrown, say ":" separated join of object's fields) and use it as the key instead. If this does not work for you, another way may be to pass the object's properties in the URL but then I think you may need to modify the ehcache-server code.
ReplyDeleteI have to cache large static data(which need to be updated every day) and that data is used from different application server.
ReplyDeleteMy application is deployed on 5 app server in cluster enviornment. I want to use cached data from all 5 app server.I made one master app server which load cache and rest 4 app server is slave(only read that cache) .
For this I have deployed the ehcache.war on one server and cached my data on that ehcache through Http Restful .This ehcache is updated every day(using schedular for this).
Is this approach is fine or I need to use ehcache standalone server for this pupose or teracotta serve I am confused.
Kindly suggest.
Hi Akhilesh, I think your current setup should work fine, although I personally would move the cache out into its own server. Reason being that since its a cluster, all your machines are (probably?) configured similarly and have similar JVM settings. One of these machines hosts the ehcache server within the application's JVM which means that JVM probably needs (or will need as your cache grows) a larger heap than the others. Putting it into a separate ehcache server accessible from every machine in the cluster allows you to pull the caching stuff out of the app altogether. You can then also scale the cache horizontally if required in the future.
ReplyDeleteHi Sujit,
ReplyDeleteThankyou so much to provide me helpful information.
Right now I am facing some issue so please look into my setting.
If I have deployed the ehcache.war on xx.xx.xx.40:8080 server then the service URL would be xx.xx.xx.40:8080/ehcache/rest in the provided Helper method.
And following setting need to be in the ehcache.war's ehcache.xml.
...
Please suggest.
I think your configuration (probably XML?) did not make it through blogger's comment system. You may want to enclose it in <pre>...</pre> if it allows you to, else manually convert "<" to "<" and ">" to ">" before hitting submit. In any case, given the part of the question that came through, I think you can interact with the ehcache REST server over HTTP (ie use HTTP POST to write to cache and HTTP GET to read from cache. Your application does not need to know anything beyond the URL xx.xx.xx.40:8080/ehcache/rest and the cache name.
ReplyDelete