Redis as a cache for Java servlets
I’ve been refactoring an application recently to move away from a proprietary and inflexible in memory datastore. The drawbacks of the proprietary datastore included the fact that the content was static. The only way to update data involved a build and replication process that took much longer than the stakeholders were willing to wait. The main selling point in favor of the in memory datastore was that it is blazing fast. And I mean blazing fast.
My choice for a replacement datastore technology is MongoDB. MongoDB worked great, but the profiling and performance comparison naturally showed that the in memory solution out performed the MongoDB solution. Communication with MongoDB for every request was obviously much slower than the previous in memory datastore solution, and the response time was less consistent from one request to another.
Caching for performance
When the data being used to generate a response changes infrequently, it’s generally bad design to serve dynamic content on every page load. Enter caching. There are a host of caching approaches covering everything from reverse proxies, like varnish, to platform specific solutions, like EHCache.
As a first stab, I chose a golden oldie, memcached, and an up and coming alternative, redis. There’s some lively discussion online about the performance differences between these two technologies. Ultimately I chose Redis due to the active development on the platform and the feature set.
Basic cache
In Java there are a handful of available Redis drivers. I started with the Jedis client. In order to use Jedis, I added this to my pom.xml.
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.0.0</version> <type>jar</type> <scope>compile</scope> </dependency> |
I then modified my basic Servlet to init a JedisPool and use jedis to cache the values I was retrieving from MongoDB. Here’s what my class ended up looking like.
package com.danielwatrous.cachetest; import com.google.inject.Guice; import com.google.inject.Injector; import com.danielwatrous.linker.domain.WebLink; import com.danielwatrous.linker.modules.MongoLinkerModule; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class BuildCnavLink extends HttpServlet { private static Injector hbinjector = null; private static JedisPool pool = null; @Override public void init() { hbinjector = Guice.createInjector(new MongoLinkerModule()); pool = new JedisPool(new JedisPoolConfig(), "localhost", 6379); } @Override public void destroy() { pool.destroy(); } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/xml;charset=UTF-8"); PrintWriter out = response.getWriter(); String value = ""; Jedis jedis = null; try { jedis = pool.getResource(); String cacheKey = getCacheKey (request.getParameter("country"), request.getParameter("lang"), request.getParameter("company")); value = jedis.get(cacheKey); if (value == null) { WebLink webLink = hbinjector.getInstance(WebLink.class); webLink.setLanguage(request.getParameter("lang")); webLink.setCountry(request.getParameter("country")); webLink.setCompany(request.getParameter("company")); value = webLink.buildWebLink(); jedis.set(cacheKey, value); } } finally { pool.returnResource(jedis); } try { out.println("<link>"); out.println("<url>" + value + "</url>"); out.println("</link>"); } finally { out.close(); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } protected String getCacheKey (String country, String lang, String company) { String cacheKey = country + lang + company; return cacheKey; } } |
Observations
It’s assumed that a combination of country, lang and company will produce a unique value when buildWebLink is called. That must be the case if you’re using those to generate a cache key.
There’s also nothing built in above to invalidate the cache. In order to validate the cache it may work to build a time/age check. There may be other more sophisticated optimistic or pessimistic algorithms to manage cached content.
In the case above, I’m using redis to store a simple String value. I’m also still generating a dynamic response, but I’ve effectively moved the majority of my data calls to redis.
Conclusion
As a first stab, this performs on par with the proprietary in memory solution that we’re replacing and the consistency from one request to another is very tight. Here I’m connecting to a local redis instance. If redis were on a remote box, network latency may erase these gains. Object storage or serialization may also affect performance if it’s determined that simple String caching isn’t sufficient or desirable.
Resources
http://www.ibm.com/developerworks/java/library/j-javadev2-22/index.html
https://github.com/xetorthio/jedis/wiki/Getting-started
Its pretty good example. Thanks 🙂
If possible, provide example on, how to clear data from redis cache. take bean as parameter?
In the example above I would add a line right below where I set the value using jedis.set(cacheKey, value).
jedis.expire(cacheKey, 15);
That would cause it to expire after 15 seconds.
[…] Radis as cache for Servlets […]
[…] Radis as cache for Servlets […]
[…] Radis as cache for Servlets […]