Request-Scoped Caching
Request-Scoped Caching
Cortex caches by request scope using the Spring AOP framework. Caches live for the duration of the request's lifecycle. Once the request processes, cache entries are evicted; thereby, freeing up the allocated memory. Request-Scoped Caching is controlled by @CacheResult and @CacheRemove annotations.
Enabling Request-Scoped Caching
- Add AspectJ to your resource's Spring configuration in resource_name/src/main/resources/OSGI-INF/blueprint/applicationContext-resource_name-resource.xml
<!-- This tells Spring to interpret the AspectJ annotations. However, it still uses pure Spring AOP at runtime. --> <aop:aspectj-autoproxy />
- Add the following maven dependencies to your resource's pom.xml:
<dependency> <groupId>com.elasticpath.rest</groupId> <artifactId>ep-rest-spi</artifactId> <version>0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.elasticpath.rest</groupId> <artifactId>ep-rest-spi-impl</artifactId> <version>0-SNAPSHOT</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.servicemix.bundles</groupId> <artifactId>org.apache.servicemix.bundles.aspectj</artifactId> </dependency>
- Add cacheKeyManager, cacheRemoveService, and cacheResultService imports to the resource's blueprint configuration file in resourcename/src/main/resources/OSGI-INF/blueprint/resourcename-blueprint.xml
<reference id="cacheKeyManager" interface="com.elasticpath.rest.cache.CacheKeyManager"/commerce-legacy/> <reference id="cacheRemoveService" interface="com.elasticpath.rest.cache.CacheRemoveService"/commerce-legacy/> <reference id="cacheResultService" interface="com.elasticpath.rest.cache.CacheResultService"/commerce-legacy/>
- Create CacheRemoveAspect and CacheResultAspect Spring AOP classes in
resourcename/src/main/java/*
CacheResultAspect.java import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import com.elasticpath.rest.cache.CacheResult; import com.elasticpath.rest.cache.CacheResultService; /** * This is an AOP Aspect, it adds caching to methods that are annotated with {@link CacheResult}. */ @Singleton @Aspect @Named("cacheResultAspect") public class CacheResultAspect { @Named("cacheResultService") private CacheResultService cacheResultService; /** * This method will generate a key from the String parameters and Class name and use it to cache the result of the annotated method. * * @param joinPoint the method that we are decorating. * @param cacheResult the cache annotation. * @return the cache hit or result of the method invocation. * @throws Throwable if the method can not proceed. */ @Around("@annotation( cacheResult ) ") public Object decorateMethodWithCaching(final ProceedingJoinPoint joinPoint, final CacheResult cacheResult) throws Throwable { return cacheResultService.decorateMethodWithCaching(joinPoint, cacheResult); } }
CacheRemoveAspect.java import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import com.elasticpath.rest.cache.CacheRemove; import com.elasticpath.rest.cache.CacheRemoveService; /** * This is an AOP Aspect, it adds caching to methods that are annotated with {@link com.elasticpath.rest.cache.CacheRemove}. */ @Singleton @Aspect @Named("cacheRemoveAspect") public class CacheRemoveAspect { @Named("cacheRemoveService") private CacheRemoveService cacheRemoveService; /** * This method will generate a key from the String parameters and Class name and use it to cache the result of the annotated method. * * @param joinPoint the method that we are decorating. * @param cacheRemove the cache remove annotation. * @return the cache hit or result of the method invocation. * @throws Throwable if the method can not proceed. */ @Around("@annotation( cacheRemove ) ") public Object decorateMethodWithCaching(final ProceedingJoinPoint joinPoint, final CacheRemove cacheRemove) throws Throwable { return cacheRemoveService.decorateMethodWithCaching(joinPoint, cacheRemove); } }
Your resource is now caching enabled.
Caching Method Responses
@CacheResult public Customer getCustomer(CustomerId customerId){}
Invalidating Caches on Methods
@CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(CustomerId customerId) {} @CacheRemove(typesToInvalidate = Customer.class) public void deleteAllCustomers() {}For nested generics, this class should be the unchecked simplest form. For ExecutionResult return type, the wrapper class should be ignored and the nested type used. The following translation table applies:
Return-type | typesToInvalidate | Description |
---|---|---|
Store | Store.class | No generic type used thus class type can be directly derived from the return type. |
ExecutionResult<Store> | Store.class | ExecutionResult wrapper is omitted, the type-hint rule for the declared generic type applies. |
Map<String, List<String>> | Map.class | Nested generic type definitions are flatten and class type. |
ExecutionResult<List<String>> | List.class | When defining ExecutionResult in combination with a nested generic type omitting ExecutionResult and generic type flattening applies. |
Tuning Request-Scoped Caching Performance
To achieve the best request-scoped caching performance, use the tuning techniques described below.
Defining Key Variants
Key variants have to be unique otherwise inconsistencies might occur.
class Customer { String storeCode,userId; //key variant - unique identifier String guid //key variant - unique identifier } @CacheResult//first call goes here public Customer findCustomerByUserId(String storeCode, String userId) {} @CacheResult//second call goes here, leverages key variant caching public Customer findCustomerByGuid(String guid) {}To enable the cache to use key variant optimization, register the domain object and its key variants by implementing the CacheKeyVariants interface and registering this interface with the CacheKeyManager. For example:
@Singleton @Named public final class CustomerCacheKeyVariants implements CacheKeyVariants<Customer> { @Inject public CustomerCacheKeyVariants( @Named("cacheKeyManager") final CacheKeyManager cacheKeyManager) { //register the CacheKeyVariant to use it cacheKeyManager.register(this); } @Override public Collection<Object[]> get(final Customer customer) { return Arrays.asList( // the keys have be unique identifier, otherwise inconsistencies might occur new Object[]{customer.guid}, new Object[]{customer.storeCode, customer.userId} ); } @Override public Class<Customer> getType() { return Customer.class; } }
How Cache Invalidation Works
Request-scoped caches are divided into cache regions, each region represents the class that instantiated the cache. Cached values are only available to the class that originated the cache. The same applies for invalidation, only the originating class can invalidate the cache.
Getting the Best Cache Invalidation Performance
We achieve the best performance results by invalidating the least amount of cache entries as possible. Below is a list of some important cache invalidation approaches.
We match the invalidation method parameters, so only cached entries with the same input parameters are removed from the cached.
@CacheResult() public Customer getCustomerById(CustomerId customerId){} @CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(CustomerId customerId){} //both method have the same input parameter //when calling deleteCustomer(CustomerId(123)) only the //value associated to CustomerId(123) will be removed
We use partial parameter matches, so invalidation method parameters are a subset of the cached method parameters.
@CacheResult() public Customer getCustomerByFullName(String name,String preName){} @CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(String preName){} //parameters of delete are subset of the get method //when calling deleteCustomer("Albert") all Customers with the preName Albert will be removed
@CacheResult(uniqueIdentifier="customerId") public Customer getCustomerById(CustomerId customerId){} @CacheRemove(typesToInvalidate = Customer.class, uniqueIdentifier="customerId") public void deleteCustomer(CustomerId customerId){} //all Customer objects registered with the defined unique identifier will be removed
By not using any of the above strategies, all cached entries in a region can be invalidated and evicted, causing performance hit.
@CacheResult() public Customer getCustomerById(String storeCode,String userId){} @CacheRemove(typesToInvalidate = Customer.class) public void removeCustomer(String customerName){} //In this case all cached Customer objects will be removed from the cache.
Avoiding Cache Inconsistencies
Treat Cache Values as Immutable Objects
Modifying cached values is a bad practice that can lead to inconsistencies. Either use immutable types as values, which is the preferred way, or treat cached values as if they were immutable and create new objects on mutation.
Avoiding Method Signature Collision
@CacheResult(uniqueIdentifier = "default-customer-address") public Address getDefaultCustomerAddress(CustomerId customerId) {} @CacheResult(uniqueIdentifier = "shipping-address") public Address getShippingAddress(CustomerId customerId) {}
Limitations of Annotation Based Request-Scoped Caching
- Annotations won't work when calling the annotated methods from other methods inside the class applying the cache. The same rules applies when using inheritance.
@CacheResult public Integer myMethod() {} public Integer myMethodDelegate() { return myMethod(); } // call to myMethodDelegate will not result in calling the cached Version of myMethod!
- Mixed usage of either wrapping the return object in an ExecutionResult or not is not possible.
@CacheResult public ExecutionResult<Customer> getCustomerById(CustomerId customerId) {} @CacheResult public CEDomainObject method2(final String customerCachKeyVariant) {}
Manual Caching
In cases where annotation caching isn't appropriate, you can cache manually. To use manual caching, inject an instance of the cacheKeyBasedRequestCache bean.
@Named("cacheKeyBasedRequestCache") private TypedRequestCache<CacheKey> requestCache public void method() { String value = "cache me"; requestCache.putCacheEntry(new CacheKey(this.getClass(),"my-cache-entry", String.class), value); //do some operations requestCache.invalidateCacheEntries(new CacheKey(this.getClass(),"my-cache-entry", String.class))}
Logging Caching
<logger name="com.elasticpath.rest.resource.integration.epcommerce.repository" level="TRACE"/commerce-legacy/>Cache logging dramatically decreases cache performance as it will log a lot of information.
<logger name="com.elasticpath.rest.cache.guava.impl" level="TRACE"/commerce-legacy/>
Configuring Request-Scoped Caching
By default, the request-scoped cache is configured to evict cached entries after 60 sec and when the number of cached entries reaches 2147483647. Parameters are configurable via the RelOS Request Cache OSGi configuration in the Apache Felix Web Console. Caches are destroyed and rebuilt when configuration values are updated through the Felix console.
Cache Metrics Monitoring
Request-scoped cache metrics are exposed as MBeans over Cortex's JMX API. The metrics Mbeans are identified by the naming pattern EP-RelOS.*.RequestCache.*.
- evictionCount – number of times entries have been evicted
- hitCount – number of times entries have been looked up and found
- hitRate – ratio of cache request which were hits
- loadCount – number of times entries have been looked up
- missCount – number of times entries have been looked up and missed
- missRate – ratio of cache requests which weren't hits
- requestCount – number of times entries have been looked up