Use Memcached for Java enterprise performance, Part 2: Database-driven web apps

Using Memcached with Hibernate in Java web applications

However you slice it traditional caching requires performance trade-offs that some enterprise applications cannot afford. Find out for yourself why Memcached is a go-to solution for Java developers whose applications need serious scale. After first setting up spymemcached as your open source Java client for Memcached, you'll use it in two powerful application scenarios: first configuring Memcached as second-level cache for Hibernate (via hibernate-memcached), and then using it to cache the HTML generated for each web page.

We concluded the first half of this tutorial with a look at using Telnet and the Memcached protocol to store and retrieve cache entries in a Memcached server. Accessing Memcached via Telnet is especially useful for debugging, but if you want to use Memcached in a Java enterprise application you'll need to use a Memcached Java client.

We'll use spymemcached, a very popular Memcached Java client, for the introductory purposes of this tutorial. Listing 1 shows spymemcached's main class, MemcachedClient.

Listing 1. MemcachedClient

public static void main(String[] args) throws Exception{
    if(args.length < 2){
        System.out.println("Please specify command line options");
        return;
    }
    MemcachedClient memcachedClient = new MemcachedClient(AddrUtil.getAddresses("127.0.0.1:11211"));
    if(commandName.equals("get")){
        String keyName= args[1];
        System.out.println("Key Name " +keyName);
        System.out.println("Value of key " +memcachedClient.get(keyName));
    }else if(commandName.equals("set")){
        String keyName =args[1];
        String value=args[2];
        System.out.println("Key Name " +keyName + " value=" + value);
        Future<Boolean> result= memcachedClient.set(keyName, 0, value);
        System.out.println("Result of set " + result.get());
    }else if(commandName.equals("add")){
        String keyName =args[1];
        String value=args[2];
        System.out.println("Key Name " +keyName + " value=" + value);
        Future<Boolean> result= memcachedClient.add(keyName, 0, value);
        System.out.println("Result of add " + result.get());
    }else if(commandName.equals("replace")){
        String keyName =args[1];
        String value=args[2];
        System.out.println("Key Name " +keyName + " value=" + value);
        Future<Boolean> result= memcachedClient.replace(keyName, 0, value);
        System.out.println("Result of replace " + result.get());
    }else if(commandName.equals("delete")){
        String keyName =args[1];
        System.out.println("Key Name " +keyName );
        Future<Boolean> result= memcachedClient.delete(keyName);
        System.out.println("Result of delete " + result.get());
    }else{
        System.out.println("Command not found");
    }
    memcachedClient.shutdown();

}

In Listing 1, we first create a MemcachedClient object with hostname:portname as its argument. Once we have the object we can start calling its methods to set and get cache entries. Note that MemcachedClient has equivalent methods for every method supported by the Memcached protocol:

  • MemcachedClient.set() is used to store a Java object into the Memcached server. In Listing 1, we stored an instance of a Contact object with the string contactId-1. The second parameter of this set method is time-in-seconds.
  • MemcachedClient.get() is used to get the value of the key from a Memcached server. In Listing 1 the value of the key is an object of Contact.java, so the client will first retrieve the value and then deserialize it and return the object. The get() method returns null if the value is not found or is expired.
  • MemcachedClient.add() is used to add an object to the cache only if it does not already exist. In Listing 1 the contactId-1 key exists in the cache already, so it won't be added.
  • MemcachedClient.replace() replaces an object with the value for the given key (if there is already such a value). In Listing 1, contactId-1 is already in the cache, so its value would be replaced.
  • MemcachedClient.delete() deletes a given key from the cache. In Listing 1, the call is used to delete the contactId-1 key.

MemcachedClient.set()

You can use MemcachedClient.set() to store a simple string or a complex object. When you store a complex object, MemcachedClient will first serialize the object and then store it. As a result, every object that you store in Memcached must be serializable, and the key must also be a string. In Listing 1, the set() method returns an object of Future<Boolean>. When we call the set method, it is executed asynchronously, so the control moves to the next line without waiting for a response from the Memcached server. If you needed to know the result of the set operation, then you would call setResult.get() on the Future object instance.

Spying under the hood: spymemcached

Before we move on to the web application exercise, let's look under the hood to see how spymemcached works. Figure 1 is a sequence diagram showing what happens in spymemcached when a client issues a get().

Figure 1. get() under the hood (click to enlarge)

Spymemcached is an asynchronous, single-threaded Memcached client. When you call any caching-related method on spymemcached's MemcachedClient, it will be handled asynchronously. The client call method handles writing the details of the operation that should be performed into a queue and returning the control back to the client making the call. The actual interaction with the Memcached server, meanwhile, is handled by a separate thread that runs in the background.

Notice that the sequence diagram in Figure 1 has two different threads. The first thread shows the method sequence for what happens when a client makes a get() call. The second thread displays a method sequence for the daemon thread that communicates with the Memcached server. Both threads are worth a closer look.

Sequence of events in a client thread

When you call MemcachedClient's get() method it takes the arguments and forwards control to the asyncGet() method on the object of Memcached class. The asyncGet() method then forwards control to either AsciiOperationFactory or BinaryOperationFactory, depending on the Memcached protocol your client uses to communicate with the server. AsciiOperationFactory is the default value.

The AsciiOperationFactory constructs an object of the command-specific operation object. In this case, since the client issued a get command, it creates an object of GetOperationImpl and returns it. The MemcachedClient.asyncGet() method then takes care of attaching a callback function to the operation. This function will be called when MemcachedClient gets data back from the server and returns a java.util.concurrent.Future object. The client uses the java.util.concurrent.Future to retrieve the data returned from server.

Once MemcachedClient has the object of GetOperationImpl, it first tries to validate the key by ensuring that the length of the key is less than 250 characters and does not contain any special characters. Once the key is validated, the next task is to figure out which server the request should go to. For that MemcachedClient passes control to an instance of the NodeLocator class with the key. The NodeLocator class calls the HashAlgorithm.hash(key) method to get the hashCode for the key. By default, NodeLocator will call the hashCode() method on the key, which is a String object. Once it has the hashCode it will divide that by the number of servers; for example, if the hashCode were 10 and the number of servers three, then the remainder would be one. So the cache entry would be located in Server 1. The MemcachedNode object representing Server 1 would be selected. The MemcachedConnection object would then add the get operation to the queue of Server 1 and return control to the client code.

Sequence of events in daemon threads

If you take a look at the MemcachedClient source code, you will notice that it implements the java.lang.Thread interface. When you create a new instance of the MemcachedClient it kicks off a new thread by calling a start() method on the current object, at which point the JVM will call a MemcachedClient run() method from the newly created thread.

Inside the run() method, MemcachedClient checks the value of the running flag. If it is true, MemcachedClient calls the handleIO() method of the MemcachedConnection object. handleIo() looks at the current job queue to get a list of pending tasks and tries to optimize them. For example, if more than one get() request is pending then this method will combine them into one call. The handleIO() method uses the java.nio methods to communicate with the server. When you call the MemcachedClient shutdown() method, it changes the value of the running flag to false, which results in stopping the run() method and the background daemon thread. It also closes the connection with the Memcached server.

Using Memcached in an enterprise web application

This section introduces you to integrating Memcached into an enterprise architecture. Using the Contact web application introduced in Listing 1, we'll add, remove, update, and view records in a CONTACT table. Next I'll show you how to use Memcached to alleviate database load by configuring it as second-level cache for Hibernate. Finally, I'll explain how to use Memcached to store custom Java objects and cache the HTML generated for each web page.

Start by downloading ManageContact-NoCaching.zip. The ManageContact application contains a ContactServlet that looks at incoming requests and decides what database interaction is required to execute each one. It then forwards control to ContactDAO in order to execute the required database interaction. ContactDAO uses Hibernate to execute select, insert, and update functions on the Contact table. When control is returned to ContactServlet, it forwards control to the appropriate JSP in order to generate markup on the web page. The ManageContact application has a Maven script that takes care of downloading all of the necessary dependencies; you can execute a mvn install command to download these dependencies and then run the application in an embedded Glassfish server. Once the server is started you should be able to access the application at http://localhost:8080/ManageContact/contact.

Enter hibernate-memcached

Most web applications spend a good chunk of their time interacting with databases. Caching data can help you speed up that interaction, as well reduce load on your database. Hibernate provides a nice interface for caching that allows you to use your own caching framework. You instruct Hibernate on which caching framework to use by specifying the name of a class that implements org.hibernate.cache.CacheProvider with the property hibernate.cache.provider_class. From there, you have two options: you can either create your own class that implements the org.hibernate.cache.CacheProvider interface and stores the cache entries in the Memcached server, or you can use the hibernate-memcached framework, which is an open source framework based on the spymemcached client. hibernate-memcached supports entity and query caching.

Configure hibernate-memcached as a caching framework

By default caching is disabled in hibernate-memcached, so our first step is to enable Hibernate's second-level cache. We'll also need to configure Memcached as a caching implementation for Hibernate. We can handle both of these requirements in the Hibernate configuration file (hibernate.cfg.xml), which is used to configure application-level settings for Hibernate:

Listing 2. Hibernate config

<property name="cache.provider_class">com.googlecode.hibernate.Memcached.MemcachedCacheProvider</property>
 <property name="hibernate.Memcached.servers">localhost:11211</property>
 <property name="hibernate.Memcached.cacheTimeSeconds">300</property>
 <property name="hibernate.Memcached.connectionFactory">BinaryConnectionFactory</property>

Now let's take a closer look at each of the properties set. You can enable the second-level cache by setting a value of cache.provider_class to com.googlecode.hibernate.Memcached.MemcachedCacheProvider. The properties that start with "hibernate.Memcached" are specific to the Memcached provider:

  • cache.provider_class: The value of this property defines which class should be used as a cache implementation. In this case we set it to com.googlecode.hibernate.Memcached.MemcacheddCacheProvider, which is provided by the hibernate-memcached framework and uses Memcached as a caching framework.
  • hibernate.Memcached.server: The properties starting with hibernate.Memcached are used by the hibernate-memcached framework. The value of hibernate.Memcached.server should be a space-delimited list of Memcached instances in host:port format. This Memcached server is running on my localhost at port 11211, so I set the value to localhost:11211, which is the default value.
  • hibernate.Memcached.cacheTimeSeconds: The value of this property defines the default number of seconds that each item should be cached. I want the CONTACT record to be cached for 300 second.
  • hibernate.Memcached.connectionFactory: This is the "simple" name of the spyMemcached ConnectionFactory class. It must be one of DefaultConnectionFactory, KetamaConnectionFactory, or BinaryConnectionFactory. The BinaryConnectionFactory performs much better by using a binary protocol

More configuration properties

See "Adding hibernate-memcached to your application" on the hibernate-memcached wiki for a list of all the configuration properties supported by hibernate-memcached.

Develop a caching strategy

Once you have enabled caching your next step is to choose which data you want to cache and what caching strategy you will use. In my case, I want to cache the data from my CONTACT table. Since that will be regularly updated, I set my cache strategy to read-write by adding a cache element in the Contact.hbm.xml, like this:

Listing 3. Configure the caching strategy

<hibernate-mapping package="com.javaworld.Memcached">
    <class name="Contact" table="CONTACT"  >
    <cache usage="read-write"/>
        <id name="contactId" column="CONTACTID">
            <generator class="increment"/>
        </id>
        <property name="firstName" column="FIRSTNAME"/>
        <property name="lastName" column="LASTNAME"/>
        <property name="email" column="EMAIl"/>
    </class>
</hibernate-mapping>

After you've updated your own Contact.hbm.xml file (which you can download with the article source), start the Memcached server by executing a Memcached -vv command. This will start Memcached in verbose mode so that it prints every client interaction on the console. Next, execute mvn clean install to start the application. Go to http://localhost:8080/ManageContact/contact and add couple of records. When you click on the record to go to the details, you should notice that no SQL query is executed; instead those records are coming back from the cache. On the Memcached server console, you should see an interaction similar to what is shown in Figure 2.

Figure 2. A view from the Memcached console (click to enlarge)

Using Memcached for server responses

So far you've seen how to reduce the load on your database by using Memcached as a second-level cache in Hibernate. Not all application scenarios are quite so simple, however. For instance, how should you handle web pages that process and display data from a web service? You'll find your application using CPU-intensive logic to build markup for every response, which probably would be better off cached. If you cache the generated markup then the next request for that markup will return from the cache instead of going to a servlet.

The first step is to build a simple Servlet filter to intercept the request. Start by copying the CachingResponseWrapper.java and CachingResponseWriter.java into a com.javaworld.Memcached.filter package. Together these two classes will collect the responses generated by ContactServlet into a String.

Next, create a CacheFilter.java in the com.javaworld.Memcached.filter package and change its doFilter() method so that it looks like this:

Listing 4. CacheFilter.java

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.debug("Inside CachingFilter.doFilter() " );
        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest)request;
            HttpServletResponse httpServletResponse = (HttpServletResponse)response;
            ObjectPool<MemcachedClient> MemcachedClientPool = MemcachedHelper.getMemcachedConnectionPool();
            MemcachedClient  MemcachedClient = MemcachedClientPool.borrowObject();
            StringBuffer cacheKeyBuffer = new StringBuffer();
            cacheKeyBuffer.append(httpServletRequest.getContextPath());
            cacheKeyBuffer.append(httpServletRequest.getServletPath());
            if(httpServletRequest.getQueryString() != null){
                cacheKeyBuffer.append("?");
                cacheKeyBuffer.append(httpServletRequest.getQueryString());
            }
            
            String cacheKey = httpServletResponse.encodeURL(cacheKeyBuffer.toString());
            System.out.println ("Get Path Info  " + cacheKey);
            String cachedResponse =(String) MemcachedClient.get(cacheKey);
            
            if( cachedResponse == null){
                System.out.println("Response is not cached forwarding control to servlet");
                CachingResponseWrapper cachingResponseWrapper =new CachingResponseWrapper((HttpServletResponse)response);
                chain.doFilter(request, cachingResponseWrapper);
                CachingResponseWriter collectResponseWriter = (CachingResponseWriter)cachingResponseWrapper.getWriter();
                String collectedResponseStr = collectResponseWriter.getCollectedResponse();//.replaceAll("\n", "") ;
                System.out.println( "Set value in the Memcached for key " + httpServletResponse.encodeURL(collectedResponseStr));
                
                System.out.println("Result of set" + MemcachedClient.set(cacheKey, 0, collectedResponseStr).get());
                //MemcachedClient.flush().get();
            }else{
                System.out.println("Returning cached response ");
                response.setContentType("text/html");
                response.getWriter().println(cachedResponse);
            }
            //MemcachedClient.flush().get();
            MemcachedClientPool.returnObject(MemcachedClient);
        } catch (NoSuchElementException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Note that in the doFilter() method you're first building the URL that the client used to access the ContactServlet; this URL is used as a key for caching. Once the URL is built, you'll execute MemcachedClient.get(cacheKey) to check if the response for the given URL is already cached. If it is, then return it; if it isn't, then wrap the response object into a CachingResponseWriter and pass control to a servlet that will generate the necessary markup. After the control returns from the servlet, call a collectResponseWriter.getCollectedResponse() method. This method will return the response generated by the servlet as a String. Take that response and save it in your cache with its URL as the key. The next time this query is used, the response will come from your cache and not from the servlet.

Next you'll need to change the web.xml for the ManageContact web application in order to intercept requests going to ContactServlet and cache them. You do this by declaring the CachingFilter and applying it to ContactServlet, as shown in Listing 5.

Listing 5. A CachingFilter for ContactServlet

<web-app>
    <display-name>Archetype Created Web Application</display-name>
 <filter>
        <filter-name>CachingFilter</filter-name>
        <display-name>CachingFilter</display-name>
        <filter-class>com.javaworld.Memcached.filter.CachingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CachingFilter</filter-name>
        <servlet-name>ContactServlet</servlet-name>
    </filter-mapping> 
    <servlet>
        <servlet-name>ContactServlet</servlet-name>
        <servlet-class>com.javaworld.Memcached.ContactServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ContactServlet</servlet-name>
        <url-pattern>/contact</url-pattern>
    </servlet-mapping>
</web-app>
Figure 3. The Memcached console displays a caching request (click to enlarge)

This caching solution works to reduce high CPU usage on the application sever. It can be used to cache a full-page response or a fragment of a page. An advantage of this approach is that you can read the response from any other client and skip going to the application server altogether.

In conclusion

If you have a web application whose traffic is increasing exponentially, then you have the best problem in the world -- but it still could be a challenge to make the application scale. One way to ensure that performance stays on track is to start caching more data. But traditional data caching steals heap space, which can affect application performance another way.

Memcached works around the problems of traditional caching by caching the data in separate process from application memory. It also lets you cache more data by distributing the cache across different machines. In this article, you've learned how to integrate Memcached into a traditional enterprise architecture. We've practiced setting up spymemcached, configuring Memcached as a caching implementation for Hibernate, and using Memcached to cache server responses. See the Resources section to learn more about using Memcached for Java applications.

Sunil Patil is a Java EE Architect working for Avnet Technology in San Francisco, California. He is the author of Java Portlets 101 (SourceBeat, April 2007) and has written numerous articles published by JavaWorld, IBM developerWorks, and O'Reilly Media. In addition to being an IBM Certified WebSphere Portal Server Application Developer and Administer, he is a Sun Microsystems Certified Java Programmer, a Web component developer, and a business component developer. You can view Sunil's blog at http://www.webspherenotes.com.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies