With advanced load balancers and decreasing hardware costs, ensuring the scalability of enterprise applications across multiple boxes is becoming more economically feasible. Ideally, an application’s TPS (transactions per second) will rise in a linear fashion as the number of servers it runs on increases. However, sooner or later you will encounter performance issues on one box that you can’t easily (or at least cheaply) alleviate by scaling across multiple servers, such as your relational database for example.
Hence, it is critical to lower your database load. In particular, you can employ efficient caching strategies that reduce the number of SQL queries and the amount of data that needs to be transferred over the wire. One powerful caching strategy that meets these requirements is to combine Hibernate cache (second-level) and Ehcache.
In this article I explain how to apply this strategy — along with the Spring Framework — to improve the scalability of your enterprise Java applications. I provide source code for a demo application as well.
Hibernate Second-level Cache and Ehcache
A typical Web request is wrapped around a filter (e.g. a Spring Hibernate session filter) that automatically opens and closes a Hibernate session during its duration. The Spring OpenSessionInViewFilter is an example of such as filter. Within each session, Hibernate maintains a first-level cache to ensure against multiple copies of the same entity within that session. This cache is discarded as soon as the session closes, and it is not persistent.
However, Hibernate also offers a second-level cache, which is persistent. It is designed with the capability to cache serialized copies of entities over longer periods of time. Hibernate’s cache mechanism is pluggable and various caching libraries offer integration modules for it. Among these, the most popular and robust is by far Ehcache — an extremely powerful caching product.
Using a local in-memory/disk cache can not only lower the load on the database, but it can also dramatically increase read speeds. In an internal test we ran at my organization, we were able to get a throughput of 20,000+ reads per second for a Hibernate entity from a local Ehcache disk cache. Retrieving it directly from the database maxed out at 5000 reads per second — a significant difference.
Ehcache can store large collections of objects in memory or automatically overflow them to disk. It also provides control over many aspects of the caching mechanism, including along others:
- How many objects you want in memory
- How long before the objects become stale and need to be automatically refreshed
- Whether the cache is disk persistent between application restarts
Configuring Spring, Hibernate and Ehcache for Improved Scalability
Let’s start off by defining the Spring Hibernate session filter I mentioned previously (OpenSessionInViewFilter) via web.xml:
<filter> <filter-name>hibernateFilter</filter-name> <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class> </filter> <filter-mapping> <filter-name>hibernateFilter</filter-name> <url-pattern>/services/*</url-pattern> </filter-mapping>
Next, in the Spring beans.xml configuration we will specify a typical Hibernate session factory. The important parts are bolded in the example below:
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="hibernateProperties"> <props> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop> <prop key="hibernate.connection.isolation">3</prop> <prop key="hibernate.cache.region.factory_class">net.sf.ehcache.hibernate.EhCacheRegionFactory</prop> <prop key="net.sf.ehcache.configurationResourceName">/com/developer/ehcache.xml</prop> </props> </property> <property name="annotatedClasses"> <list> <value>com.developer.article.model.Person</value> </list> </property></bean>/property></bean>
The hibernate.cache.region.factory_class property configures Ehcache as the second-level cache provider. The net.sf.Ehcache.configurationResourceName property tells Ehcache the classpath location of the configuration file that fine-tunes its setting. Note that this file must be in the classpath, hence it’s usually embedded in the JAR/WAR file itself.
Fine-tuning Ehcache Settings
Here’s our sample com.developer.ehcache.xml configuration file from the setup in the previous section:
<Ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" /></Ehcache>
Ehcache creates a separate cache for every Hibernate entity, with a name equivalent to its full path. Hence, you can override every entity’s cache settings if the need arises. However, in most cases the defaultCache settings are sufficient as a template. In this example we’ve chosen settings to:
- cache no more than 10,000 instances of a single entity in memory
- force the entity to refresh from the database every 120 seconds
- let it overflow to disk if we reach more than our maxElementsInMemory limit
The list of Ehcache configuration options is incredibly extensive and goes far beyond this basic example.
Configuring Hibernate/JPA Entities
Just because we’ve configured Hibernate and Ehcache together, that does not mean that every single entity is now automatically cached. Every single Hibernate entity must be explicitly flagged via the @Cache
annotation:
@Entity @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)public class Person {}
The Hibernate documentation explains the different cache concurrency strategies, but NONSTRICT_READ_WRITE is the most common one. If you are running in a full JTA environment, you may choose TRANSACTIONAL instead.
Spring, Hibernate and Ehcache in Practice
Attached to this is article is a sample application that shows how this all works in practice. It exposes a simple REST service for a basic Person entity under http://localhost:8080/services/person and requires Maven to run. The sample Log4J configuration is set for TRACE for both Hibernate and Ehcache in order to show the most information about what’s going on under the hood.
When you start the application via mvn jetty:run
it will show a long list of trace messages, out of which the key ones are:
Cache region factory : net.sf.ehcache.hibernate.EhCacheRegionFactory...Creating EhCacheRegionFactory from a specified resource: /com/developer/ehcache.xml. ...Building cache for entity data [com.developer.article.model.Person]Couldn't find a specific Ehcache configuration for cache named [com.developer.article.model.Person]; using defaults.started Ehcache region: com.developer.article.model.Person
Now via the standard Linux curl
command (or any other REST client on your PC platform) we will send a GET
request to our REST Web services to get a JSON representation of a single Person entity that we have in our database:
curl http://localhost:8080/services/person/6{"Person":{"id":6,"firstName":"First 3","lastName":"Last 3"}}
Let’s look at the server-side log to see what happened on that first request:
adding entity to second-level cache: [com.developer.article.model.Person#6]done materializing entity [com.developer.article.model.Person#6]
Hibernate retrieved our entity from the database, stored it in the cache automatically and returned it to us. Now let’s issue the same curl
command again and see what happens:
loading entity: [com.developer.article.model.Person#6]attempting to resolve: [com.developer.article.model.Person#6]assembling entity from second-level cache: [com.developer.article.model.Person#6]
This time Hibernate automatically found our entity in the cache and loaded it directly from memory without hitting the database — which was the main reason we wanted to use second-level cache in the first place.
Effective Strategy but Nothing Is Free
This of course sounds great and looks easy. However, all of this comes at a price, namely increased complexity (especially during testing). By relying on cached entities, you risk having an out-of-sync cache when an update happens in the database via a different request.
As such, you must analyze every entity that can be cached to determine whether serving a potentially stale copy of it is acceptable. Stale copies may be fine for fairly static entities that rarely change, but they would not be appropriate for something such as a bank user’s Account object, which should always reflect the latest state in the database.
However, options are available for handling those situations. For example, you can configure Ehcache to do multicasting over the network in order to update the caches automatically on all servers whenever an entity gets updated. You even can wrap such a cache refresh within a JTA transaction context to ensure all the caches get updated together. But that’s a topic for an upcoming article.