JCache and Beyond

❗️

This is a legacy Apache Ignite documentation

The new documentation is hosted here: https://ignite.apache.org/docs/latest/

Overview

Apache Ignite data grid is an implementation of the JCache (JSR 107) specification. JCache provides a very simple to use, but yet very powerful API for data access. However, the specification purposely omits any details about data distribution and consistency to allow vendors enough freedom in their own implementations.

With JCache support you get the following:

  • Basic Cache Operations
  • ConcurrentMap APIs
  • Collocated Processing (EntryProcessor)
  • Events and Metrics
  • Pluggable Persistence

In addition to JCache, Ignite provides ACID transactions, data querying capabilities (including SQL), various memory models, queries, transactions, and more.

IgniteCache

The IgniteCache interface is a gateway into the Ignite cache implementation and provides methods for storing and retrieving data, executing queries, including SQL, iterating and scanning, etc.

IgniteCache is based on JCache (JSR 107), so at the very basic level the APIs can be reduced to the javax.cache.Cache interface. However, the IgniteCache API also provides functionality that facilitates features outside of the JCache spec, like data loading, querying, asynchronous mode, etc.

You can get an instance of IgniteCache directly from Ignite:

Ignite ignite = Ignition.ignite();

// Obtain instance of cache named "myCache".
// Note that different caches may have different generics.
IgniteCache<Integer, String> cache = ignite.cache("myCache");

❗️

Cache Name Restriction

Because of a known issue, cache names cannot contain characters that are prohibited in file names (such as '/' on Linux or '' on Windows).

Dynamic Cache

You can also create an instance of the cache on the fly, in which case Ignite will create and deploy the cache across all server cluster members that match the cache node filter. After a dynamic cache has been started, it will also be automatically deployed on all the newly joined server cluster members that match the cache node filter.

Ignite ignite = Ignition.ignite();

CacheConfiguration cfg = new CacheConfiguration();

cfg.setName("myCache");
cfg.setAtomicityMode(TRANSACTIONAL);

// Create cache with given name, if it does not exist.
IgniteCache<Integer, String> cache = ignite.getOrCreateCache(cfg);

📘

XML Configuration

All caches defined in Ignite Spring XML configuration on any cluster member will also be automatically created and deployed on all the cluster servers (no need to specify the same configuration on each cluster member).

Basic Operations

Here are some basic JCache atomic operation examples:

try (Ignite ignite = Ignition.start("examples/config/example-cache.xml")) {
    IgniteCache<Integer, String> cache = ignite.cache(CACHE_NAME);
 
    // Store keys in cache (values will end up on different cache nodes).
    for (int i = 0; i < 10; i++)
        cache.put(i, Integer.toString(i));
 
    for (int i = 0; i < 10; i++)
        System.out.println("Got [key=" + i + ", val=" + cache.get(i) + ']');
}
// Put-if-absent which returns previous value.
Integer oldVal = cache.getAndPutIfAbsent("Hello", 11);
  
// Put-if-absent which returns boolean success flag.
boolean success = cache.putIfAbsent("World", 22);
  
// Replace-if-exists operation (opposite of getAndPutIfAbsent), returns previous value.
oldVal = cache.getAndReplace("Hello", 11);
 
// Replace-if-exists operation (opposite of putIfAbsent), returns boolean success flag.
success = cache.replace("World", 22);
  
// Replace-if-matches operation.
success = cache.replace("World", 2, 22);
  
// Remove-if-matches operation.
success = cache.remove("Hello", 1);

🚧

Deadlock

If batch operations (such as IgniteCache#putAll, IgniteCache#invokeAll, etc.) are performed in parallel, then keys should be ordered in the same way to avoid deadlock. Use TreeMap instead of HashMap to guarantee consistent ordering. Note that this is true for both ATOMIC and TRANSACTIONAL caches.

EntryProcessor

Whenever doing puts and updates in cache, you are usually sending the full object state across the network. EntryProcessor allows for processing data directly on primary nodes, often transferring only the deltas instead of the full state.

Moreover, you can embed your own logic into EntryProcessors, for example, taking a previous cached value and incrementing it by 1.

IgniteCache<String, Integer> cache = ignite.cache("mycache");

// Increment cache value 10 times.
for (int i = 0; i < 10; i++)
  cache.invoke("mykey", (entry, args) -> {
    Integer val = entry.getValue();

    entry.setValue(val == null ? 1 : val + 1);

    return null;
  });
IgniteCache<String, Integer> cache = ignite.cache("mycache");

// Increment cache value 10 times.
for (int i = 0; i < 10; i++)
  cache.invoke("mykey", new EntryProcessor<String, Integer, Void>() {
    @Override 
    public Object process(MutableEntry<Integer, String> entry, Object... args) {
      Integer val = entry.getValue();

      entry.setValue(val == null ? 1 : val + 1);

      return null;
    }
  });

📘

Atomicity

EntryProcessors are executed atomically within a lock on the given cache key.

Asynchronous Support

Just like all distributed APIs in Ignite, IgniteCache extends the IgniteAsynchronousSupport interface and can be used in asynchronous mode.

// Enable asynchronous mode.
IgniteCache<String, Integer> asyncCache = ignite.cache("mycache").withAsync();

// Asynhronously store value in cache.
asyncCache.getAndPut("1", 1);

// Get future for the above invocation.
IgniteFuture<Integer> fut = asyncCache.future();

// Asynchronously listen for the operation to complete.
fut.listenAsync(f -> System.out.println("Previous cache value: " + f.get()));

Using Ignite as JCache Provider

JCache API accepts a provider-specific configuration if a cache instance needs to be created using the JCache manager. This allows you to take advantage of the distributed caching capability that Ignite provides, without any modifications to your existing code if you're migrating from a different JCache implementation.

Here is an example of how to provide an Ignite cache configuration using JCache manager:

// Get or create a cache manager.
CacheManager cacheMgr = Caching.getCachingProvider().getCacheManager();

// This is an Ignite configuration object (org.apache.ignite.configuration.CacheConfiguration).
CacheConfiguration<Integer, String> cfg = new CacheConfiguration<>();

// Specify cache mode and/or any other Ignite-specific configuration properties.
cfg.setCacheMode(CacheMode.PARTITIONED);

// Create a cache based on the configuration created above.
Cache<Integer, String> cache = cacheMgr.createCache("aCache", cfg);

// Cache operations
Integer key = 1;
String value = "11";
cache.put(key, value");
System.out.println(cache.get(key));

Additionally, the CachingProvider.getCacheManager(..) method accepts a provider-specific URI which, for Ignite, should point to the XML configuration file. For example:

// Get or create a cache manager.
CacheManager cacheMgr = Caching.getCachingProvider().getCacheManager(
  Paths.get("path/to/ignite/xml/configuration/file").toUri(), null);

// Get a cache defined in the configuration file provided above.
Cache<Integer, String> cache = cacheMgr.getCache("myCache");

// Cache operations
Integer key = 1;
String value = "11";
cache.put(key, value");
System.out.println(cache.get(key));

It's worth mentioning that the above Cache<K, V> instance will only support features of javax.cache.Cache type.

To use extended features provided by Ignite, such as SQL, scan, or continuous queries, you need to convert javax.cache.Cache instance to IgniteCache instance, like so:

// Get or create a cache manager.
CacheManager cacheMgr = Caching.getCachingProvider().getCacheManager(
  Paths.get("path/to/ignite/xml/configuration/file").toUri(), null);

// Get a cache defined in the configuration file provided above.
Cache<Integer, String> cache = cacheMgr.getCache("myCache");

// Obtain org.apache.ignite.IgniteCache instance.
IgniteCache igniteCache = (IgniteCache) cache;

// Ignite specific cache operations
//......