Friday, January 18, 2013

Fun with @Cacheable


From Spring 3.1.0.RELEASE onwards the @Cacheable annotation has been added to the spring-context jar. We will go through the motions of demonstrating how it can be leveraged through a contrived example of calculating the fibonacci numbers. We will have a service interface that is supposed to get the fibonacci number given an index over and over.
public interface FibonacciService {
    long getFibonacci(int index);
}
We will have three different service implementations. One will be a simpleton who will plainly calculate the number - called SimpletonFibonacciServiceImpl.
    public long getFibonacci(int index) {

        if (index == 0 || index == 1) {
            return 1;
        }

        long fiboMinusOne = getFibonacci(index - 1);
        long fiboMinusTwo = getFibonacci(index - 2);

        return fiboMinusOne + fiboMinusTwo;
    }
Our driver will make it go through its paces by making it calculate the 45th number of the fibonacci series.
    private static void doWorkWithFiboService(FibonacciService fibonacciService) {
        // First one
        doHeavyLifting(fibonacciService);
        // Some more
        doHeavyLifting(fibonacciService);
        // Come on... one last
        doHeavyLifting(fibonacciService);
        // Now flush
        fibonacciService.flushFibonacciCache();
        // Do some more
        doHeavyLifting(fibonacciService);
        // Be polite - clean out at the end - flush!
        fibonacciService.flushFibonacciCache();
    }

    private static void doHeavyLifting(FibonacciService fibonacciService) {
        StopWatch sw = new StopWatch();
        sw.start();
        fibonacciService.getFibonacci(45);
        sw.stop();

        LOG.info(fibonacciService.getClass().getCanonicalName() + " took "
                + sw.getTotalTimeSeconds() + " seconds");
    }
It roughly takes 7 seconds on a stock host to perform each computation (measured via org.springframework.util.StopWatch which is a good tool for poor man's benchmarking if you've not used it already).
[ 2013-01-18 10:49:04,006 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.SimpletonFibonacciServiceImpl took 7.212 seconds
[ 2013-01-18 10:49:11,236 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.SimpletonFibonacciServiceImpl took 7.23 seconds
[ 2013-01-18 10:49:18,465 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.SimpletonFibonacciServiceImpl took 7.229 seconds
[ 2013-01-18 10:49:18,466 [main] driver.SimpletonFibonacciServiceImpl.flushFibonacciCache():27  INFO ]: No-op
[ 2013-01-18 10:49:25,695 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.SimpletonFibonacciServiceImpl took 7.229 seconds
[ 2013-01-18 10:49:25,695 [main] driver.SimpletonFibonacciServiceImpl.flushFibonacciCache():27  INFO ]: No-op
To demonstrate the use of cache, we will have the second guy will use a concurrent map as the backing store for caching numbers once they are computed.
    @Cacheable("concurrentMapFibonacci")
    public long getFibonacci(int index) {

        if (index == 0 || index == 1) {
            return 1;
        }

        long fiboMinusOne = getFibonacci(index - 1);
        long fiboMinusTwo = getFibonacci(index - 2);

        return fiboMinusOne + fiboMinusTwo;
    }
The service has been annotated with the @Cacheable annotation. The annotation needs to be provided with the name of the backing cache store. The cache store definition (viz. concurrentMapFibonacci) is defined in the Spring context file as
    <bean id="concurrentMapFibonacci"
        class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" />
    <bean id="concurrentMapCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches">
            <set>
                <ref bean="concurrentMapFibonacci" />
            </set>
        </property>
    </bean>
To enable the processing of the annotation, we need to specify the directive:
    <cache:annotation-driven proxy-target-class="true" cache-manager="concurrentMapCacheManager" />
This is very similar to the tx:annotation-driven directive that we use to enable processing for the @Transactional annotation. Here we ask Spring to process all beans in the container for the @Cacheable annotation and use the bean named "concurrentMapCacheManager" to act as the CacheManager (which is of type SimpleCacheManager). The processing can be done via proxies or aspectj byte code weaving. For simple use cases, we prefer using proxies which is the default mode (we will cover the aspectj angle in the next post). In using proxies, by default JDK proxies are created which fail if the bean to be proxied doesn't implement any interface. In contrast CGLIB based proxies work on POJO's with the exception of final classes and non-default constructor classes (because they work by subclassing the POJO). We ask it to use CGLIB based proxies by specifying the proxy-target-class="true" switch.
[ 2013-01-18 11:17:24,707 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.ConcurrentMapFibonacciServiceImpl$$EnhancerByCGLIB$$5ea80813 took 7.595 seconds
[ 2013-01-18 11:17:24,708 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.ConcurrentMapFibonacciServiceImpl$$EnhancerByCGLIB$$5ea80813 took 0.001 seconds
[ 2013-01-18 11:17:24,708 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.ConcurrentMapFibonacciServiceImpl$$EnhancerByCGLIB$$5ea80813 took 0.0 seconds
[ 2013-01-18 11:17:24,709 [main] driver.ConcurrentMapFibonacciServiceImpl.flushFibonacciCache():30  INFO ]: Royal ConcurrentMap flush
[ 2013-01-18 11:17:32,283 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.ConcurrentMapFibonacciServiceImpl$$EnhancerByCGLIB$$5ea80813 took 7.574 seconds
[ 2013-01-18 11:17:32,283 [main] driver.ConcurrentMapFibonacciServiceImpl.flushFibonacciCache():30  INFO ]: Royal ConcurrentMap flush
The first call takes the full time to calculate the fibonacci number, but the subsequent calls just fetch the numbers from the cache and hence finish instantaneously. Let's now explore the flushing or eviction of entries from the cache. The cache manager that we have used here is a SimpleCacheManager which provides a no-frills management (we will see fancy controls in management via EhCacheCacheManager later on). To evict entries from a cache on a particular call, we have the @CacheEvict annotation.
    @CacheEvict(allEntries = true, beforeInvocation = true, value = "concurrentMapFibonacci")
    public void flushFibonacciCache() {
        LOG.info("Royal ConcurrentMap flush");
    }
In this annotation we have asked Spring to evict all entries in the cache named "concurrentMapFibonacci" before said method is invoked. We can choose to evict bespoke keys via the keys directive (using SpEL). If the method has any arguments, those are used as keys for the entries to be evicted (ofcourse, in the absence of allEntries = true). So as expected, when the flush call is made, the cache goes cold, and the subsequent fetch again takes the full 7 odd seconds. It is important to note that though we are making recursive calls, the only key-value that got cached is the final one and not the intermediate ones. The reason being the mode that we chose. Proxy based solutions will only intercept calls that are being made from one spring bean to another and self calls are not processed (recursive or private/protected/public calls to the same class). In the next post, we will use aspectj to get a real live and kicking cache that will also house the intermediate entries and observe the improvement empirically.
The previous eviction policy was somewhat straight-jacketed. Who wants to call a method to flush caches periodically? Not me, because frankly it can be handled by a better cache store and an intelligent manager. Here is where ehcache comes in.
    @Cacheable("fibonacciEhcache")
    public long getFibonacci(int index) {

        if (index == 0 || index == 1) {
            return 1;
        }

        long fiboMinusOne = getFibonacci(index - 1);
        long fiboMinusTwo = getFibonacci(index - 2);

        return fiboMinusOne + fiboMinusTwo;
    }
To ask Spring to use ehcache as the underlying store, we specify so in the Spring context file:
    <bean id="fibonacciEhcache"
        class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="ehcache.xml" />
    </bean>
    <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="fibonacciEhcache"/>
    </bean>
We ask it to create a cache store named fibonacciEhcache who is managed by the EhCacheCacheManager.
    <cache:annotation-driven cache-manager="ehcacheManager" proxy-target-class="true"/>
Further configuration for the cache is available in the ehcache config file named "ehcache.xml" available on the classpath which looks something like:
<?xml version="1.0" encoding="UTF-8"?>
<!-- To know more about the available configuration. Refer http://ehcache.org/ehcache.xml -->
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd" monitoring="autodetect"
    updateCheck="false" dynamicConfig="false">

    <diskStore path="java.io.tmpdir" />

    <cache name="fibonacciEhcache" maxEntriesLocalHeap="10000"
        maxEntriesLocalDisk="1000" eternal="false"
        diskSpoolBufferSizeMB="20" timeToIdleSeconds="300"
        timeToLiveSeconds="600" memoryStoreEvictionPolicy="LFU"
        transactionalMode="off">
        <persistence strategy="localTempSwap" />
    </cache>

    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        diskSpoolBufferSizeMB="30"
        maxElementsOnDisk="1000"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU" />

</ehcache>
The cache named fibonacciEhcache is has been thus configured to keeping the backing disk store on the java.io.tmpdir location (by default /tmp on linux) and have the cache purged every 10 minutes (via the timeToLiveSeconds="600"). Other explanations can be found in the documentation of the sample ehcache.xml file.
[ 2013-01-18 11:38:12,689 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.EhcacheFibonacciServiceImpl$$EnhancerByCGLIB$$a1d7290c took 7.598 seconds
[ 2013-01-18 11:38:12,690 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.EhcacheFibonacciServiceImpl$$EnhancerByCGLIB$$a1d7290c took 0.001 seconds
[ 2013-01-18 11:38:12,690 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.EhcacheFibonacciServiceImpl$$EnhancerByCGLIB$$a1d7290c took 0.0 seconds
[ 2013-01-18 11:38:12,697 [main] driver.EhcacheFibonacciServiceImpl.flushFibonacciCache():30  INFO ]: Royal Ehcache flush
[ 2013-01-18 11:38:20,286 [main] driver.SpringCacheTesterDriver.doHeavyLifting():52  INFO ]: com.kilo.driver.EhcacheFibonacciServiceImpl$$EnhancerByCGLIB$$a1d7290c took 7.588 seconds
[ 2013-01-18 11:38:20,287 [main] driver.EhcacheFibonacciServiceImpl.flushFibonacciCache():30  INFO ]: Royal Ehcache flush
The runs reflect roughly the same behavior as with the concurrent map usage.
This will serve as a primer and we will have some more fun in the next post where we explore slightly more involved use cases.
References:
  • http://static.springsource.org/spring/docs/3.1.0.M1/spring-framework-reference/html/cache.html
  • http://ehcache.org/ehcache.xml
  • Testbed available at https://github.com/kilokahn/spring-testers/tree/master/spring-cache-tester

No comments:

Post a Comment