Application cache

Now that our web requests have been compressed and cached, the next step we can take to reduce server load is to put the results of costly operations in a cache. The Twitter search takes some time and will consume our application request ratio on the Twitter API. With Spring, we can easily cache the search and return the same result each time the search is called with the same parameters.

The first thing that we need to do is activate Spring caching with the @EnableCache annotation. We also need to create a CacheManager that will resolve our caches. Let's create a CacheConfiguration class in the config package:

package masterSpringMvc.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

@Configuration
@EnableCaching
public class CacheConfiguration {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        simpleCacheManager.setCaches(Arrays.asList(
                new ConcurrentMapCache("searches")
        ));
        return simpleCacheManager;
    }
}

In the previous example, we use the simplest possible cache abstraction. Other implementations are also available, such as EhCacheCacheManager or GuavaCacheManager, which we will use in a moment.

Now that we have configured our cache, we can use the @Cacheable annotation on our methods. When we do that, Spring will automatically cache the result of the method and associate it with the current parameters for retrieval.

Spring needs to create a proxy around beans whose methods are cached. This typically means that calling a cached method inside of the same bean will not fail to use Spring's cache.

In our case, in the SearchService class, the part where we call the search operations, would benefit greatly from caching.

As a preliminary step, it would be good to put the code responsible for creating the SearchParameters class in a dedicated object called SearchParamsBuilder:

package masterSpringMvc.search;

import org.springframework.social.twitter.api.SearchParameters;

import java.util.List;
import java.util.stream.Collectors;

public class SearchParamsBuilder {

    public static SearchParameters createSearchParam(String searchType, String taste) {
        SearchParameters.ResultType resultType = getResultType(searchType);
        SearchParameters searchParameters = new SearchParameters(taste);
        searchParameters.resultType(resultType);
        searchParameters.count(3);
        return searchParameters;
    }

    private static SearchParameters.ResultType getResultType(String searchType) {
        for (SearchParameters.ResultType knownType : SearchParameters.ResultType.values()) {
            if (knownType.name().equalsIgnoreCase(searchType)) {
                return knownType;
            }
        }
        return SearchParameters.ResultType.RECENT;
    }
}

This will help us to create search parameters in our service.

Now we want to create a cache for our search results. We want each call to the Twitter API to be cached. Spring cache annotations rely on proxies to instrument the @Cacheable methods. We therefore need a new class with a method annotated with the @Cacheable annotation.

When you use the Spring abstraction API, you don't know about the underlying implementation of the cache. Many will require both the return type and the parameter types of the cached method to be Serializable.

SearchParameters is not Serializable, that's why we will pass both the search type and the keyword (both strings) in the cached method.

Since we want to put the LightTweets object in cache, we want to make them Serializable; this will ensure that they can always be written and read from any cache abstraction:

public class LightTweet implements Serializable {
    // the rest of the code remains unchanged
}

Let's create a SearchCache class and put it in the search.cache package:

package masterSpringMvc.search.cache;

import masterSpringMvc.search.LightTweet;
import masterSpringMvc.search.SearchParamsBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.social.TwitterProperties;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.social.twitter.api.SearchParameters;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.social.twitter.api.impl.TwitterTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class SearchCache {
    protected final Log logger = LogFactory.getLog(getClass());
    private Twitter twitter;

    @Autowired
    public SearchCache(TwitterProperties twitterProperties) {
        this.twitter = new TwitterTemplate(twitterProperties.getAppId(), twitterProperties.getAppSecret());
    }

    @Cacheable("searches")
    public List<LightTweet> fetch(String searchType, String keyword) {
        logger.info("Cache miss for " + keyword);
        SearchParameters searchParam = SearchParamsBuilder.createSearchParam(searchType, keyword);
        return twitter.searchOperations()
                .search(searchParam)
                .getTweets().stream()
                .map(LightTweet::ofTweet)
                .collect(Collectors.toList());
    }
}

It can't really get simpler than that. We used the @Cacheable annotation to specify the name of the cache that will be used. Different caches may have different policies.

Note that we manually created a new TwitterTemplate method rather than injecting it like before. That's because we will have to access the cache from other threads a little bit later. In Spring Boot's TwitterAutoConfiguration class, the Twitter bean is bound to the request scope and is therefore not available outside of a Servlet thread.

With those two new objects, the code of our SearchService class simply becomes this:

package masterSpringMvc.search;

import masterSpringMvc.search.cache.SearchCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Profile("!async")
public class SearchService implements TwitterSearch {
    private SearchCache searchCache;

    @Autowired
    public SearchService(SearchCache searchCache) {
        this.searchCache = searchCache;
    }

    @Override
    public List<LightTweet> search(String searchType, List<String> keywords) {
        return keywords.stream()
                .flatMap(keyword -> searchCache.fetch(searchType, keyword).stream())
                .collect(Collectors.toList());
    }
}

Note that we annotated the service with @Profile("!async"). This means that we only create this bean if the profile async is not activated.

Later, we will create another implementation of the TwitterSearch class to be able to switch between the two.

Neat! Say we restart our application and try a big request such as the following:

http://localhost:8080/search/mixed;keywords=docker,spring,spring%20boot,spring%20mvc,groovy,grails

It will take a little time at first, but then our console will display the following log:

2015-08-03 16:04:01.958  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for docker
2015-08-03 16:04:02.437  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for spring
2015-08-03 16:04:02.728  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for spring boot
2015-08-03 16:04:03.098  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for spring mvc
2015-08-03 16:04:03.383  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for groovy
2015-08-03 16:04:03.967  INFO 38259 --- [nio-8080-exec-8] m.search.cache.SearchCache               : Cache miss for grails

After that, if we hit refresh, the result will be displayed immediately and no cache miss will be seen in the console.

That's it for our cache, but there is much more to the cache API. You can annotate methods with the following:

  • @CachEvict: This will remove an entry from the cache
  • @CachePut: This will put the result of a method into a cache without interfering with the method itself
  • @Caching: This regroups the caching annotation
  • @CacheConfig: This points to different caching configurations

The @Cacheable annotation can also be configured to cache results on certain conditions.

Note

For more information on Spring cache, please see the following documentation:

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html

Cache invalidation

Currently, search results will be cached forever. Using the default simple cache manager doesn't give us a lot of options. There is one more thing that we can do to improve our application caching. Since we have Guava in our classpath, we can replace the existing cache manager in the cache configuration with the following code:

package masterSpringMvc.config;

import com.google.common.cache.CacheBuilder;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.guava.GuavaCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfiguration {

    @Bean
    public CacheManager cacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager("searches");
        cacheManager
                .setCacheBuilder(
                        CacheBuilder.newBuilder()
                                .softValues()
                                .expireAfterWrite(10, TimeUnit.MINUTES)
                );
        return cacheManager;
    }
}

This will build a cache expiring after 10 minutes and using soft values, meaning that the entries will be cleaned up if the JVM runs low on memory.

Try to fiddle around with Guava's cache builder. You can specify a smaller time unit for your testing, and even specify different cache policies.

Distributed cache

We already have a Redis profile. If Redis is available, we could also use it as our cache provider. It would allow us to distribute the cache across multiple servers. Let's change the RedisConfig class:

package masterSpringMvc.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

import java.util.Arrays;

@Configuration
@Profile("redis")
@EnableRedisHttpSession
public class RedisConfig {

    @Bean(name = "objectRedisTemplate")
    public RedisTemplate objectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Primary @Bean
    public CacheManager cacheManager(@Qualifier("objectRedisTemplate") RedisTemplate template) {
        RedisCacheManager cacheManager = new RedisCacheManager(template);
        cacheManager.setCacheNames(Arrays.asList("searches"));
        cacheManager.setDefaultExpiration(36_000);
        return cacheManager;
    }
}

With this configuration, if we run our application with the "Redis" profile, the Redis cache manager will be used instead of the one defined in the CacheConfig class since it is annotated with @Primary.

This will allow the cache to be distributed in case we want to scale on more than one server. The Redis template is used to serialize the cache return values and parameters, and will require objects to be Serializable.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.36.38