Skip to content

Caching strategies

Alexander Kochurov edited this page Mar 25, 2014 · 4 revisions

Caching strategies determine how caches are created, filled and cleaned.

There are few abstraction levels in MxCache that one can use to implement own strategies. Let's go through them.

###Implementing Storage The simplest way to implement you own caching strategy is to simply extend one of <Key><Value>Storage interfaces (e.g. ObjectObjectStorage, ObjectStorage). If you are not sure which to implement, implement these two: ObjectObjectStorage and ObjectStorage - one for caches with parameters, and one for caches without it.

This approach gives you less control than other methods, but it's the simplest way to implement your strategies. MxCache takes care of dependency tracking and everything else in this case.

MxCache also gives you a flexibility when creating instances: you can get CacheDescriptor and CacheContext of the cache being created by just defining a constructor taking one or both of this parameters.

ObjectObjectStorage<E, F> interface has very few methods:

  • Object load(E) - should return stored value. Don't forget that load should return Storage.UNDEFINED constant in case of no value was saved for this key.
  • void save(E key, F value) - should save given value.
  • void clear() - clears cache.
  • int size() - should return approximate size.

ElementLocked Variant MxCache allows you to control locking of your cache manually.

For doing this you should extend <Key><Value>Storage interface from another package, and implement two more methods:

lock(E)
unlock(E) 

Binding It Just add an annotation @UseStorage(YourStorageClassGoesHere.class) or use mxcache.xml property "storage".

###Writing Custom StorageFactory You can create a custom StorageFactory that will take care of creating Storage of proper type. StorageFactory allows you to control which Storage type should be created, e.g. create an instance of ObjectStorage for caches without arguments and instances of ObjectObjectStorage for caches with arguments.

You can use the same CacheDescriptor and CacheContext injection via constructor.

Binding It Just specify annotation @UseStorageFactory(YouStorageFactoryClassGoesHere.class) or use mxcache.xml property "storage.factory".

Extending AbstractCache If you need more control, you can extend your cache directly from Abstract<Key><Value>Cache. This approach allows you to alter the whole way you cache functiones.

###Writing Custom CacheManager CacheManager is some kind of factory for caches. See StorageFactory approach for details.

There is a standard DefaultStrategy strategy, which by default handles all caches.

To use a different strategy it is neccesary to specify its class either in a special annotation @ Strategy, or in the configuration file MxCache XML Configuration:

@Cached
@Strategy(some.Strategy.class)
public Value someMethod(...) {...}

###Creating your own strategy

To develop your strategy it’s enough to implement its storage class.

There are two types of storages: with and without locking.

When using a locked storage, the storage is required to implement one of the interfaces [Xxx] YyyElementLockedStorage (where Xxx and Yyy are primitive name or «Object»), providing methods for locking/unlocking cache keys. Methods lock (Xxx) and unlock (Xxx) accept the key as an argument, i.e. lock can be implemented for individual keys, but it is not an indispensable condition. Mxcache is fully responsible for calling these methods, i.e. it is guaranteed that before any operations with storage the corresponding method lock (Yyy) will be called, and after the completion - unlock (Xxx) .. When using the storage without locking it is obliged to implement one of the interfaces [Xxx] YyyStorage (where Xxx and Yyy - primitive name or «Object»), in this case mxCache is fully responsible for the lock.

For methods that implement the interface [Xxx] Yyy [ElementLocked] Storage there is no need to produce the locking manually.

It's not of necessity to implement all interfaces XxxYyy[ElementLocked]Storage, Object[Yyy]Storage, [Xxx]ObjectStorage or ObjectObjectStorage may be applied even for caches using primitives as keys or values. However, this can lead to performance degradation because of boxing and unboxing and also to increased memory consumption..

If the strategy is applied to the method which takes few parameters, then they all will be wrapped with a Tuple object, and transferred to storage in this form.

Example:

 /** this storage will store only the last requested item  */
public class OnlyLastIntObjectStorage<T> implements IntObjectStorage<T> {
  private int key;
  // must be initialized, otherwise the first request will be returned null вместо правильного значения!!!
  private Object value = UNDEFINED;
  public Object load(int key) {
    // if there’s no value found in cache – return special value UNDEFINED.
    return this.key == key ? value : UNDEFINED;
  }
  public void save(int key, T value) {
    this.key = key;
    this.value = value;
  }
  public int size() {
    return value == UNDEFINED ? 0 : 1;
  }
  public void clear() {
    value = UNDEFINED;
  }
}

###Legacy

  • Implement interface CacheManager (it is recommended to extend AbstractCacheManager). The class must implement the following methods
  • createCache(T owner, DependencyNode dependencyNode) - this method should instantiate cache instance for given owner
  • Class<? extends Cache> getImplementation() - the method should return an implementation class cache; it is used only in JMX, may not coincide with the actual class (for example, while using decorators the class that is wrapped can be returned).
  • Implement CachingStrategy that returns proper cache manager.

In case of an incorrect cache configuration it is not recommended to throw an exception; it’s better to log a message.

Strategy is responsible for correct handling of the @HashingStrategy and @HashedArray annotations

DefaultHashingStrategyFactory class provides a standard way of creating caching strategy for methods with annotation

What if I want to add my own annotations to define hashing strategies for the standard caching strategy? Extend DefaultStrategy class in your strategy and use the constructor that takes the strategy factory.

Create your own strategy factory by extending AbstractHashingStrategyFactory (or DefaultHashingStrategyFactory) and by overriding findStrategyClass metod, which must find a strategy class for a given set of annotations.

###How to get custom settings? Custom settings can be specified in MxCache XML Configuration and/or through annotations.

To obtain properties, you should use the descriptor (CacheDescriptor) that is passed to strategy/store/cache manager.

In com.maxifier.mxcache.provider package there are two classes for typed properties work:

  • StrategyProperty - describes a simple property that has no analogue via annotations;
  • AnnotationProperty - describes a property that can be set via the annotation.

To create instances of properties you can use the factory methods StrategyProperty.create(name, type [default] [, type annotation, the name of the attribute in the abstract])

In CacheDescriptor there are methods getProperty() permitting to get value for StrategyProperty and AnnotationProperty

To use AnnotationProperty it is necessary to determine descendant that will implement the getFromAnnotation() method, that receives the value of the annotation. If this method returns null, then the value will be retrieved from MxCache XML Configuration.

Example:

 @Retention(RetentionPolicy.RUNTIME)
public @interface SomePropertyAnnotation {
  int value();
}
StrategyProperty<Integer> MY_ANNOTATION_PROPERTY = StrategyProperty.create("mystrategy.property", int.type, -1, SomePropertyAnnotation.class, "value");
@Cached
@SomePropertyAnnotation(3)
public Value cachedMethod() { ... }
... inside of strategy ...
int property = descriptor.getProperty(MY_ANNOTATION_PROPERTY);
...

CacheDescriptor takes care of all the necessary conversions. The following property types are supported:

  • String;
  • Boolean;
  • Integer;
  • Long;
  • Float;
  • Double;
  • Class;
  • enums;
  • arrays of all these types.

It is recommended that you name your settings so that they could not interfere with the settings of other strategies as strategies can be combined.

What if these settings are not enough for me or my cache implies its own configuration in xml?

Then the best option is to place your cache configuration in META-INF, and MxCache XML Configuration (or annotations) and specify only the name of your file. (note, that you’ll have to download it by yourself in the strategy)

###How to set a strategy for all caches with certain annotation? Use MxCache XML Configuration. In a jar file, add a configuration file similar to the following:

 <mxcache>
  <rule>
    <selector>
      <annotated>my.package.MyAnnotation</annotated>
    </selector>
    <strategy>my.package.MyStrategy</strategy>
  </rule>
</mxcache>

This file will tie your strategy to all methods with your annotation.

###What if I need to initialize the strategy on my own way? InstanceProvider creates strategies. You can either replace it with your own implementation (recommended method), or obtain an instance via InstanceProviderImpl.getInstance() and register the already created strategy instance in it.

The changes will affect only the newly created objects, the existing ones will continue to work as before.