Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
The Java Tips blog is the continuation of one of JavaWorld's most popular long-running series. Find new Java development tips published here weekly and peruse the Java Tips Archive for older tips submitted by your peers in the Java developer community. Submit a Tip: jwedit@javaworld.com Subscribe to the feed
Caching is a vital cross-cutting concern for improving the performance of enterprise applications. If you're building an application with the Spring framework, declarative caching services using Spring Modules offer an easy way to add and tune caching functionality without touching an application's code. Fundamentally, the approach works very well in achieving its intent; however, it falls short when scaled in real-world scenarios. This tip explains real-world problems faced when using such an approach, and extends the existing declarative model to address them.
In enterprise applications, there is often a need to load reference and lookup data into a cache. This is very easily achievable using any of the the approaches mentioned in the Spring Module specification and in Alex Ruiz's article "Declarative caching services for Spring." These approaches use Spring AOP to identify the target method call and store the returned Java object in the cache. The cache itself is a glorified Java Map object, the key for which is created using the vendor's cache key generation process. The cache can be viewed as a collection of cache entries; each entry represents a key-value pair of a Map object, as illustrated in Figure 1.
One problem scenario arises on application startup, and immediately afterwards. At startup, there is often a need to load reference and lookup data into a cache. This typically involves multiple back-to-back I/O operations to retrieve different datasets. Each I/O operation results in a cache event.
In a cache event scenario, a key is generated using the cache key generation process, and then the cache is checked for the existence of the key. If the key is not found, the underlying method is invoked, resulting in an I/O operation -- a call to a stored procedure, for instance, or to a SQL query. On exit, the object that the method call returns is stored in the cache using the generated key. From the user's perspective, there is a lockout period from the time just before the I/O call is made to the time just after it completes. The caching operation overhead is negligible compared to the time required for the I/O operation, as you can see in Figure 2.
In practical scenarios, this base case is extended when a number of caching events are called back-to-back, resulting in extended lockout periods. Although it might be acceptable to wait for the application to start up after all of the reference data is loaded, it becomes a huge overhead in cases where almost all datasets are candidates for preloading. This problem could be dealt with by starting the loading process in a separate thread following startup. This thread would then trigger the loading process and the cache building.
Another situation where potential problems might arise comes with cache refresh. After all, what good is cache if the data remains static? In practical scenarios, there is almost always a need to refresh the cache when the underlying data gets stale. In the declarative caching methodology, this would typically be achieved using a flush, followed by a cache event. A cache refresh event would be triggered by a polling- or interrupt-based mechanism that would result in a cache refresh at any time that the application is in use by end users. A lockout period may result in timeouts -- definitely not acceptable.
Caching on application startup poses no problems -- the application is not accessible until it has started, after all. However, if the caching is done post-startup and in a back-to-back scenario, the application does suffer from a lockout period, as it would in a cache refresh scenario. The way to solve the problem is by splitting the cache into two components -- a serving cache and a reload cache -- and using a synchronization mechanism between the two. The purpose of the reload cache is to perform a refresh -- that is, to flush and reload the data, based on an appropriate trigger that indicates that the data in cache is dirty. The serving cache always serves the client requests and does not depend on the I/O operation. It uses a poll- or interrupt-based synchronization mechanism that helps it copy the data from the reload cache; this is done in memory. From the user perspective, the serving cache is always available. The relationship between the two is illustrated in Figure 3.
If you use this approach, the lockout period is limited to the reload side of the cache, as described above. The time required to perform in-memory synchronization is negligible even in clustered environments, and hence the lockout period is negligible. The timeline diagram in Figure 4 depicts the flow of events.
Alex Ruiz's article explains the use of Spring Modules to achieve caching in enterprise applications. There are three approaches to do so, as described in the Spring Module specification:
BeanNameAutoProxyCreatorI will demonstrate an implementation of the extended caching concept using the per-bean configuration approach, and compare it with standard declarative caching as I explain how the extended version works. (You could also implement this extended caching model with source-level metadata attributes or BeanNameAutoProxyCreator, as there are no changes in the underlying Spring Modules.)
You begin by defining the service interface. Listing 1 illustrates how this would be done with declarative caching; Listing 2 shows you how it would work with extended declarative caching. Notice that the RC postfix has been defined for refresh/reload operations.
public interface CacheService {
DataMap getXXXMap(String region,Long tradeDate);
void flushXXXMap(String region,Long tradeDate);
}
public interface CacheService {
DataMap getXXXMap(String region,Long tradeDate);
DataMap getXXXMapRC(String region,Long tradeDate);
void flushXXXMapRC(String region,Long tradeDate);
}
Next, you need to implement the cache service. Listing 3 shows how this would be done in traditional declarative caching; Listing 4 illustrates our new extended declarative caching model.
public class CacheServiceImpl implements CacheService {
private XXXDao XXXDao;
public void setXXXDao(XXXDao dao) {
XXXDao = dao;
}
public void setXXXDao(XXXDao dao) {
XXXDao = dao;
}
public DataMap getXXXMap(String region, Long tradeDate) {
System.out.println("getXXXMap: region=[" + region
+ "] tradeDate=[" + tradeDate + "]");
return XXXDao.getXXXMap(region, tradeDate);
}
public void flushXXXMap(String region, Long tradeDate) {
System.out.println("flushXXXMap:region=[" + region
+ "] tradeDate=[" + tradeDate + "]");
...
}
public class CacheServiceImpl implements CacheService {
private XXXDao XXXDao;
public void setXXXDao(XXXDao dao) {
XXXDao = dao;
}
public void setXXXDao(XXXDao dao) {
XXXDao = dao;
}
public DataMap getXXXMap(String region, Long tradeDate) {
System.out.println("getXXXMap: region=[" + region
+ "] tradeDate=[" + tradeDate + "]");
return XXXDao.getXXXMap(region, tradeDate);
}
public DataMap getXXXMapRC(String region, Long tradeDate) {
System.out.println("getXXXMapRC: region=[" + region
+ "] tradeDate=[" + tradeDate + "]");
return XXXDao.getXXXMap(region, tradeDate);
}
public void flushXXXMapRC(String region, Long tradeDate) {
System.out.println("flushXXXMapRC:region=[" + region
+ "] tradeDate=[" + tradeDate + "]");
...
}
Now you need to define the per-bean configuration, as shown in Listings 5 and 6 (again, the first listing shows how it would work under traditional declarative caching, the second under extended declarative caching). Notice that the names of the serving and reload caches are different.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:coherence="http://www.springmodules.org/schema/coherence"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springmodules.org/schema/coherence http://www.springmodules.org/schema/cache/springmodules-tangosol.xsd">
<coherence:config failquietly="false">
<coherence:proxy id="cacheServiceTarget" refid="cacheServiceImpl">
<!-- CACHING & PARTIAL FLUSHING -->
<coherence:caching methodname="getXXXMap" cachename="repl-XXXCache">
<!-- FLUSHING -->
<coherence:flushing methodname="flushXXXMap" cachenames="repl-XXXCache" when="before">
...
</coherence:proxy>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:coherence="http://www.springmodules.org/schema/coherence"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springmodules.org/schema/coherence http://www.springmodules.org/schema/cache/springmodules-tangosol.xsd">
<coherence:config failquietly="false">
<coherence:proxy id="cacheServiceTarget" refid="cacheServiceImpl">
<!-- CACHING & PARTIAL FLUSHING -->
<coherence:caching methodname="getXXXMap" cachename="repl-XXXCache">
<coherence:caching methodname="getXXXMapRC" cachename="repl-XXXCacheRC">
...
<!-- FLUSHING -->
<coherence:flushing methodname="flushXXXMapRC" cachenames="repl-XXXCacheRC" when="before">
...
</coherence:proxy>
</beans>
Next, under extended declarative caching, you need to reload the cache, as illustrated in Listing 7. (Note that this would be unnecessary with ordinary declarative caching.)
this.cacheService.flushXXXMap(
DataBlockConstants.GLOBAL_REGION, null);
this.cacheService.getXXXMap(
DataBlockConstants.GLOBAL_REGION, null);
this.cacheService.flushXXXMapRC(
DataBlockConstants.GLOBAL_REGION, null);
this.cacheService.getXXXMapRC(
DataBlockConstants.GLOBAL_REGION, null);
Finally, in extended declarative caching, you need a mechanism for synching the reload and serving caches. This is illustrated in Listing 8.
NamedCache XXXReloadCache = CacheFactory
.getCache("repl-XXXCacheRC");
NamedCache XXXServingCache = CacheFactory
.getCache("repl-XXXCache");
Map XXXServingCacheBuffer = new HashMap();
for (Iterator _i = XXXServingCache.keySet().iterator(); _i
.hasNext();) {
Object _key = _i.next();
Object _value = XXXServingCache.get(_key);
XXXServingCacheBuffer.put(_key, _value);
}
// System.out.println("XXXServingCacheBuffer="
// + XXXServingCacheBuffer);
Map XXXReloadCacheBuffer = new HashMap();
for (Iterator _i = XXXReloadCache.keySet().iterator(); _i
.hasNext();) {
Object _key = _i.next();
Object _value = XXXReloadCache.get(_key);
XXXReloadCacheBuffer.put(_key, _value);
}
// System.out.println("XXXReloadCacheBuffer="
// + XXXReloadCacheBuffer);
XXXServingCache.clear();
XXXServingCache.putAll(XXXReloadCacheBuffer);
The above steps help to implement the design approach. The client's only interface is with the serving cache, which does not depend on the I/O operations to finish. This gives the perception that the cache is available at all times.
Declarative caching services for Spring are a non-intrusive approach that addresses the caching aspect in enterprise applications. The design outlined in this tip attempts to provide a practical, real-world solution to problems that emerge with this caching approach as applications scale in cache size, leaving the underlying caching concepts as is. It definitely opens up a door for improvements in the next version of Spring Modules around ideas of cache refresh.
Sameer Padwal has a background as a system and software architect, with more than 13 years of experience in the finance services, banking, and insurance industries. Sameer holds several Sun Java Certifications, as well as an M.S. in computer science from the Stevens Institute of Technology.