Coherence query cache technique using refresh-ahead
A few days ago I read an excellent post by Martin Elwin on creating a query cache using Coherence. To summarize, he uses a Coherence Filter (an object that contains query criteria) as a key and the results of the query (in this case, a set of keys for entries that match the criteria) as the value of a cache. When using Coherence (as in any data grid product) key based access is orders of magnitude faster (and of greater importance, far more scalable) than data retrieval with a query, so I think this is a great technique.
Reading through his code, I thought about ways in which Coherence can do even more of the work. This can be accomplished through the use of a CacheLoader. A CacheLoader in Coherence is used to load data for a cache from an external resource (normally a database) in the case of a cache miss. Therefore subsequent requests for that key result in a cache hit.
Many Coherence users of CacheLoaders and CacheStores take advantage of the asynchronous write-behind capabilities to scale writes to a database or a file system. However, one of the most underused (and one of the best, IMHO) features of Coherence is the ability to refresh the cache from the data source asynchronously. For example, if a cache entry is read from the database and the cache is configured to expire after 5 minutes, Coherence can be configured to fetch the data from the database asynchronously before the 5 minute expiry. This is known as the refresh ahead factor. If I configure the factor to .5, this means that any client request for this key that occurs after the entry is 2 minutes and 30 seconds old will (a) return the current value for the key and (b) request that the entry be refreshed in a background thread. If no client requests happen between the 2.5 and 5 minute mark, the item expires as normal and the next request results in a synchronous load.
So if we put these concepts together, we can create a generic CacheLoader that executes queries for any cache, expires the results after a configureable amount of time, and (after the initial request) refreshes the query results ahead of time so that no client thread has to wait for the results. Best of all, this all happens in the grid, so clients don’t have to worry about the mechanics behind it all.
Here is an example: consider an application with a Person object that looks like this:
public class Person implements Serializable { public Person(String sName, String sPhone) { m_sName = sName; m_sPhone = sPhone; } public String getName() { return m_sName; } public String getPhone() { return m_sPhone; } public String toString() { return "Person{" + "'" + m_sName + '\'' + ", phone: " + m_sPhone + '}'; } private String m_sName; private String m_sPhone; }
The cache that we’ll use to store person:
<cache-mapping> <cache-name>person</cache-name> <scheme-name>person-cache-scheme</scheme-name> </cache-mapping> ... <distributed-scheme> <scheme-name>person-cache-scheme</scheme-name> <service-name>PersonDistributedCache</service-name> <backing-map-scheme> <local-scheme /> </backing-map-scheme> </distributed-scheme>
Pretty straight forward so far. Objects of this type will be stored in a distributed cache. Now, in our application we will be doing queries often for area code, such as:
Filter filter = new LikeFilter("getPhone", "917%");
This will give us all Persons with a New York area code.
Now, the configuration for the query cache:
<cache-mapping> <cache-name>query-*</cache-name> <scheme-name>query-cache-scheme</scheme-name> </cache-mapping> ... <distributed-scheme> <scheme-name>query-cache-scheme</scheme-name> <service-name>QueryCacheDistributedCache</service-name> <backing-map-scheme> <read-write-backing-map-scheme> <internal-cache-scheme> <local-scheme> <expiry-delay>1m</expiry-delay> </local-scheme> </internal-cache-scheme> <cachestore-scheme> <class-scheme> <class-name>com.tangosol.examples.QueryCacheLoader</class-name> <init-params> <init-param> <param-type>string</param-type> <param-value>{cache-name}</param-value> </init-param> </init-params> </class-scheme> </cachestore-scheme> <read-only>true</read-only> <refresh-ahead-factor>.5</refresh-ahead-factor> </read-write-backing-map-scheme> </backing-map-scheme> <backup-count>0</backup-count> <thread-count>10</thread-count> <autostart>true</autostart> </distributed-scheme>
There’s a bit of configuration, so I’ll explain the various bits:
- We’re specifying a 1 minute delay for the expiry of this cache. This means that the results for this query will never be more than 1 minute old.
- The CacheLoader class name is specified, and we have a parameter defined that will pass along the name of the cache being used with this CacheLoader.
- The refresh-ahead-factor is configured to .5, meaning that any request for a key in the cache that is more than 30 seconds old will result in the refreshing of that cache by re-executing the query.
- The backup count is set to 0. This is transient data so if we lose a storage node in the cluster, we’re OK with losing this data since it is easy to recreate. By setting backup count to 0, this eliminates the overhead of maintaining a backup and reduces the storage requirements in the grid.
- The thread count is set to 10. I just picked an arbitrary number, but in practice this should be set high enough to ensure that there are enough threads at any given time to execute client queries simultaneously. I will point out that the asynchronous load described above happens on a separate thread.
- Also note that the service name is different than the cache storing the items we are querying: this is very important. A CacheLoader/CacheStore cannot make a call to CacheFactory.getCache() for a cache that is running under the same service, as this may lead to a deadlock.
OK, on to the CachLoader implementation!
// import statements omitted public class QueryCacheLoader extends AbstractCacheLoader { public QueryCacheLoader(String sCacheName) { azzert(sCacheName != null && sCacheName.startsWith(QUERY_PREFIX), "Cache name must start with '" + QUERY_PREFIX + "'"); m_sCacheName = sCacheName.substring(QUERY_PREFIX.length()); CacheFactory.log("Starting QueryCacheLoader for cache " + m_sCacheName, CacheFactory.LOG_DEBUG); } public Object load(Object object) { azzert(object instanceof Filter); NamedCache cache = CacheFactory.getCache(m_sCacheName); Filter filter = (Filter)object; CacheFactory.log("Executing filter " + filter + " on " + m_sCacheName, CacheFactory.LOG_DEBUG); Set keySet = cache.keySet(filter); // set that is returned is lazily deserialized only upon iteration; // so force deserialization and place into a hash set return new HashSet(keySet); } private final String m_sCacheName; public static final String QUERY_PREFIX = "query-"; }
As we can see, the implementation is fairly straight forward. By using the naming convention “query-[cache name]“, the loader can determine which cache the query should be executed on.
From the client’s point of view, running the query is very simple:
NamedCache personQueryCache = CacheFactory.getCache("query-person"); Filter filter = new LikeFilter("getPhone", "917%"); // get set of keys that match the filter Set keySet = (Set) personQueryCache.get(filter); // retrieve values for keys; this may result in a near cache hit Map queryResults = personCache.getAll(keySet); for (Iterator i = queryResults.values().iterator(); i.hasNext();) { Person p = (Person) i.next(); System.out.println(p.toString()); }
This technique will of course only work with the following preconditions:
- The queries that are executed are consistent in terms of criteria (not ad-hoc)
- It is acceptable to have slightly out of date results. If this is not acceptable, a MapListener could be registered to clear out the query cache when the source cache is updated.
Martin Elwin:
Just read through it – very nice! Good idea that. Makes it even simpler from the using code. Especially good if the queries are made from many different places. Thanks!
/M
28 September 2008, 4:27 pmPadraic Hannon:
We’ve been looking at a very similar implementation to ensure fast query times. Our idea was to build these as items are inserted for common queries and do what you are doing for less common ones. Perhaps this is a good idea to put in the coherence commons? The query cache service / primary cache service separation is something that we run into for querying deconstructed object graphs, another pattern begging for a common approach.
-paddy
29 December 2008, 4:06 pmPatrick Peralta:
Hi Paddy,
We can certainly consider putting this in the Commons project. Is the above different from your implementation?
Thanks,
30 December 2008, 3:52 pmPatrick
Padraic Hannon:
We haven’t implemented a query cache in a generic fashion yet. What we have done is create precalculated results on when we load the cluster. The one thing we did do was implement a value holder pattern to enable our object graphs to be deconstructed so that each entity can exist in its own named cache. We found when walking that object graph within the cluster (for example child.getParent.getId) we ran into the re-entrant problem and had to move each named cache into its own service.
We have several use cases where we want to get parent objects based on the child object’s properties. For example, get all parent objects that have a child object with value X. Given a model where the parent has a collection of child objects and the child objects have a reference to the parent (parent.getChildren(), child.getParent()) and assuming that the parent objects and child objects are stored in separate named caches, our initial reaction was to construct a filter:
Filter filter = new EqualsFilter(“getName”, “foo”);
NamedCache cache = CacheFactory.getCache(“childCache”);
cache.addIndex(new ChainedExtractor(“getName”);
Aggregate parentAg = new DistinctValues(new ChainedExtractor(“getParent”));
Set parents = cache.aggregate(filter, parentAg);
We found it was much more performant to change the search to be more like so:
Filter filter = new EqualsFilter(“getName”, “foo”);
NamedCache cache = CacheFactory.getCache(“childCache”);
cache.addIndex(new ChainedExtractor(“getName”);
cache.addIndex(new ChainedExtractor(“getParent.getId”);
Aggregate parentAg = new DistinctValues(new ChainedExtractor(“getParent.getId”));
Set parentKeys = cache.aggregate(filter, parentAg);
Map parents = CacheFactory.getCache(“parentCache”).getAll(parentKeys)
We have created prebuild responses and have been looking at doing a query cache as you suggested. There seems to be a lot of patterns out there, such as these, that could become part of the coherence incubator project.
aloha
8 January 2009, 3:28 pmPaddy
vikas:
Thanks for sharing this. I like your approach but i thinks its same as CQC( Continious query cache).
Just wondering how is this Continious query cache(CQC).
Refernces: http://wiki.tangosol.com/display/COH32UG/Continuous+Query
Please let me know your thoughts on this.
Regards
2 October 2009, 9:57 amVikas
Patrick Peralta:
Hi Vikas,
Thanks for your comments. There is some overlapping functionality between this and CQC. Here are the ways in which they differ:
1. CQC will register a map listener internally, thus ensuring the client gets the update right away, whereas the query cache will be stale for a period of time.
2. CQC only accepts one filter at a time, the query cache can be used to cache many filters.
The CQC is a good choice for clients that need real time access to a subset of data (defined by a single filter), whereas the query cache is a good option for caching commonly executed queries.
Thanks,
3 October 2009, 7:59 pmPatrick