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
1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more