24 Performing Basic Cache Operations

You can use the Coherence APIs to perform basic cache operations.

This chapter includes the following sections:

Overview of the NamedCache API

The com.tangosol.net.NamedCache<K, V> interface is the primary interface used by applications to get and interact with cache instances.The NamedCache<K, V> interface extends other interfaces, which each provide additional cache capabilities that are unique to Coherence and used to perform data grid operations.

The extended interfaces include:

  • java.util.Map<K, V> – This interface provides basic Map methods such as get(), put(), and remove().

  • com.tangosol.net.cache.CacheMap<K, V> – This interface provides methods for getting a collection of keys (as a Map) that are in the cache and for putting objects in the cache. This interface also supports adding an expiry value when putting an entry in a cache.

  • com.tangosol.util.QueryMap<K, V> – This interface provides methods for querying the cache. See Querying Data In a Cache.

  • com.tangosol.util.InvocableMap<K, V> – This interface provides methods for server-side processing of cache data. See Processing Data In a Cache.

  • com.tangosol.util.ObservableMap<K, V> – This interface provides methods for listening to cache events. See Using Map Events.

  • com.tangosol.util.ConcurrentMap<K, V> – This interface provides methods for concurrent access such as lock() and unlock(). See Performing Transactions.

Getting a Cache Instance

To get a reference to a NamedCache instance, applications can use the Session API and the CacheFactory API.The Session API is the preferred approach for getting a cache because it offers a concise set of methods that allows for the use and injection of Coherence sessions that are non-static. In contrast, the CacheFactory API exposes many static methods that require knowledge of internal Coherence concepts and services. Lastly, the Session API allows for a more efficient lifecycle and integration with other frameworks, especially frameworks that use injection.

The following example creates a session using a default session provider then gets a reference to a NamedCache instance using the Session.getCache method. The name of the cache is included as a parameter.

import com.tangosol.net.*;
...
Session session = Session.create();
NamedCache<Object, Object> cache = session.getCache("MyCache");

The CoherenceSession class implements the Session interface and allows applications to use the new operator. For example:

import com.tangosol.net.*;
...
Session session = new CoherenceSession();
NamedCache<Object, Object> cache = session.getCache("MyCache");

The following example gets a reference to a NamedCache instance using the CacheFactory.getCache method and includes the name of the cache as a parameter:

import com.tangosol.net.*;
...
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");

Both the Session API and CacheFactory API start the underlying cache service if necessary. The cache instance is created using a cache scheme that is defined in the cache configuration file (coherence-cache-config.xml by default). The cache scheme is mapped to the name MyCache. See Configuring Caches.

A NamedCache instance can store keys and values of any type. However, applications must ensure type safety when interacting with cache entries. Applications can create NamedCache instances for specific types and Coherence also offers API-level type checking. See Using NameCache Type Checking.

Requirements for Cached Objects

Cache keys and values must be serializable (for example, java.io.Serializable or Coherence Portable Object Format serialization). Furthermore, cache keys must provide an implementation of the hashCode() and equals() methods, and those methods must return consistent results across cluster nodes. This implies that the implementation of hashCode() and equals() must be based solely on the object's serializable state (that is, the object's non-transient fields); most built-in Java types, such as String, Integer and Date, meet this requirement. Some cache implementations (specifically the partitioned cache) use the serialized form of the key objects for equality testing, which means that keys for which equals() returns true must serialize identically; most built-in Java types meet this requirement as well. See Using Portable Object Format.

Performing Cache Put Operations

Basic cache put operations are performed using the put method as defined by the Map interface.The put method adds an entry to the cache and returns the previous value for the specified key. For example:
String key = "k1";
String value = "Hello World!";
        
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);

The putAll method is used to add multiple entries to a cache in a single bulk load operation and requires the entries to be in a Map type data structure. See Pre-Loading a Cache.

Performing Cache Get Operations

Basic cache get operations are performed using the get method as defined by the Map interface.The mapped value is returned for the specified key. For example:
String key = "k1";
String value = "Hello World!";
        
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);
System.out.println(cache.get(key));

Performing Cache Remove Operations

Basic cache remove operations are performed using the remove method as defined by the Map interface.The mapping for the specified key is removed from the cache and the previous value is returned. For example:
String key = "k1";
String value = "Hello World!";

NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);
System.out.println(cache.get(key));
cache.remove(key);

Using Default Map Operations

The java.util.Map interface includes default methods for performing operations such as putIfAbsent, replaceAll, and merge, to name a few. Coherence overrides many Map method implementations to ensure these operations perform well in a distributed environment.The methods have been re-implemented to use entry processors and to take advantage of lambda expressions. The methods are available when using the NamedCache interface. For example:
String key = "k1";
String value = "Hello World!";

NamedCache<Object, Object> cache = CacheFactory.getCache("hello-example");
cache.putIfAbsent(key, value);
System.out.println(cache.get(key));

Lambda expressions can also be used to perform any required processing on the entries. For example,

cache.replaceAll((key, value) ->
   {
   value.setLastName(value.getLastName().toUpperCase());
   return value;
   });

Pre-Loading a Cache

Pre-Loading a cache is a common scenario used to populate a cache before an application uses the data.

This section includes the following topics:

Bulk Loading Data Into a Cache

The put method can be used to bulk load data into a cache. However; each call to put may result in network traffic, especially for partitioned and replicated caches. Additionally, each call to put returns the object it just replaced in the cache (as defined in the java.util.Map interface) which adds more unnecessary overhead.

public static void bulkLoad(NamedCache cache, Connection conn)
    {
    Statement s;
    ResultSet rs;
    
    try
        {
        s = conn.createStatement();
        rs = s.executeQuery("select key, value from table");
        while (rs.next())
            {
            Integer key   = new Integer(rs.getInt(1));
            String  value = rs.getString(2);
            cache.put(key, value);
            }
        ...
        }
    catch (SQLException e)
        {...}
    }

Loading the cache can be made much more efficient by using the ConcurrentMap.putAll method instead. For example:

public static void bulkLoad(NamedCache cache, Connection conn)
    {
    Statement s;
    ResultSet rs;
    Map       buffer = new HashMap();

    try
        {
        int count = 0;
        s = conn.createStatement();
        rs = s.executeQuery("select key, value from table");
        while (rs.next())
            {
            Integer key   = new Integer(rs.getInt(1));
            String  value = rs.getString(2);
            buffer.put(key, value);

            // this loads 1000 items at a time into the cache
            if ((count++ % 1000) == 0)
                {
                cache.putAll(buffer);
                buffer.clear();
                }
            }
        if (!buffer.isEmpty())
            {
            cache.putAll(buffer);
            }
        ...
        }
    catch (SQLException e)
        {...}
    }

Performing Distributed Bulk Loading

When pre-populating a Coherence partitioned cache with a large data set, it may be more efficient to distribute the work to Coherence cluster members. Distributed loading allows for higher data throughput rates to the cache by leveraging the aggregate network bandwidth and CPU power of the cluster. When performing a distributed load, the application must decide on the following:

  • which cluster members performs the load

  • how to divide the data set among the members

The application should consider the load that is placed on the underlying data source (such as a database or file system) when selecting members and dividing work. For example, a single database can easily be overwhelmed if too many members execute queries concurrently.

A Distributed Bulk Loading Example

This section outlines the general steps to perform a simple distributed load. The example assumes that the data is stored in files and is distributed to all storage-enabled members of a cluster.

  1. Retrieve the set of storage-enabled members. For example, the following method uses the getStorageEnabledMembers method to retrieve the storage-enabled members of a distributed cache.

    protected Set getStorageMembers(NamedCache cache)
            {
            return ((PartitionedService) cache.getCacheService())
               .getOwnershipEnabledMembers();
            }
    
  2. Divide the work among the storage enabled cluster members. For example, the following routine returns a map, keyed by member, containing a list of files assigned to that member.

    protected Map<Member, List<String>> divideWork(Set members, List<String> fileNames)
            {
            Iterator i = members.iterator();
            Map<Member, List<String>> mapWork = new HashMap(members.size());
            for (String sFileName : fileNames)
                {
                Member member = (Member) i.next();
                List<String> memberFileNames = mapWork.get(member);
                if (memberFileNames == null)
                    {
                    memberFileNames = new ArrayList();
                    mapWork.put(member, memberFileNames);
                    }
                memberFileNames.add(sFileName);
    
                // recycle through the members
                if (!i.hasNext())
                    {
                    i = members.iterator();
                    }
                }
            return mapWork;
            }
    
  3. Launch a task that performs the load on each member. For example, use Coherence's InvocationService to launch the task. In this case, the implementation of LoaderInvocable must iterate through memberFileNames and process each file, loading its contents into the cache. The cache operations normally performed on the client must execute through the LoaderInvocable.

    public void load()
            {
            NamedCache cache = getCache();
    
            Set members = getStorageMembers(cache);
    
            List<String> fileNames = getFileNames();
    
            Map<Member, List<String>> mapWork = divideWork(members, fileNames);
    
            InvocationService service = (InvocationService)
                    CacheFactory.getService("InvocationService");        
    
            for (Map.Entry<Member, List<String>> entry : mapWork.entrySet())
                {
                Member member = entry.getKey();
                List<String> memberFileNames = entry.getValue();
    
                LoaderInvocable task = new LoaderInvocable(memberFileNames, cache.getCacheName());
                service.execute(task, Collections.singleton(member), this);
                }
            }

Clearing Caches

The contents of a cache can be cleared using either the clear or truncate methods that are defined by the NamedCache interface.The clear method can result in significant memory and CPU overhead and is generally not recommended to clear a distributed cache. As an alternative, the truncate method can be used. For example:
String key = "k1";
String value = "Hello World!";
        
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);
System.out.println(cache.get(key));

Cache.truncate;

The truncate method makes better use of memory and CPU resources and is often the best option when clearing large caches and caches that use listeners. The truncate method is also ideal for clearing a near cache as it clears both the front map and back map. The removal of entries caused by the truncate method are not observable by listeners, triggers, and interceptors. However, a CacheLifecycleEvent event is raised to notify all subscribers of the execution of this operation.

Releasing Caches

Applications that no longer require a cache should use the CacheFactory.release method to release local resources associated with the specified instance of the cache.Releasing a cache makes it no longer usable but does no affect the contents of a cache or other references to the cache across the cluster. For example:
String key = "k1";
String value = "Hello World!";
        
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);
System.out.println(cache.get(key));

CacheFactory.releaseCache(cache);

Destroying Caches

Caches that are created using the CacheFactory class can be destroyed using the CacheFactory.destroy method.The destroy method destroys the specified cache across the entire cluster. References to the cache are invalidated; the cached data is cleared; and, all resources are released. The destroy method is often preferred over the NamedCache.clear method, which can be both a memory and CPU intensive task in a distributed environment. For example:
String key = "k1";
String value = "Hello World!";
        
NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
cache.put(key, value);
System.out.println(cache.get(key));

CacheFactory.destroyCache(cache);

Closing Sessions

Applications should close a session when the session is no longer required.When a session is closed, all the resources that are associated with the session are also closed and references to those resources are no longer valid. An illegal state exception is thrown if an application attempts use a closed session or its resources.

Use the close method to close a session. For example:

String key = "k1";
String value = "Hello World!";

try(Session session = Session.create()) {
   NamedCache<Object, Object> cache = session.getCache("MyCache");
   cache.put(key, value);
   System.out.println(cache.get(key));
   session.close();
} 
catch (Exception e){
}

Performing NameCache Operations Asynchronously

The com.tangosol.net.AsyncNameCache<K, V> interface allows cache operations to be completed in parallel.The interface makes use of the Java CompletableFuture class, which provides completion and/or exception callbacks, chaining multiple asynchronous calls to execute one after another, and waiting for all the calls executing in parallel to complete. Performing NameCache operations asynchronously can improve throughput and result in more responsive user interfaces. The Coherence examples provide additional examples of performing asynchronous cache operations. See Coherence Asynchronous Features Example in Installing Oracle Coherence.

To perform asynchronous cache operations, you must use the CompletableFuture class and implement the Future interface. For example:

import com.tangosol.net.AsyncNamedCache;
import com.tangosol.net.CacheFactory;
import com.tangosol.net.NamedCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
 
public class HelloWorld {
 
    public static void main(String[] args) throws ExecutionException,
       InterruptedException {
       
       String key = "k1";
       String value = "Hello World!";
       
       NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache");
       AsyncNamedCache<Object, Object> as = cache.async();
       
       Future future = as.put(key, value);
       future.get();
             
       CompletableFuture cf = as.get(key);
        
       System.out.println(cf.get());
        
       CompletableFuture cfremove = as.remove(key);
        
       System.out.print("Removing key/value: " + cfremove.get() + "\n" );
       System.out.println("The key/value is: " + future.get());
    }
}

Using NameCache Type Checking

Coherence includes the ability to request strongly-typed NamedCache instances when using either the Session or CacheFactory API by using explicit types.By default, NamedCache<Object, Object> instances return. This is the most flexible mechanism to create and use NamedCache instances; however, it is the responsibility of the application to ensure the expected key and value types when interacting with cache instances.

The following example stores objects of any type for use by an application:

NamedCache<Object,Object> cache = session.getCache("MyCache");

or,

NamedCache<Object,Object> cache = CacheFactory.getCache("MyCache");

The getCache method can be used to request a NamedCache instance of a specific type, including if necessary, without type checking. The TypeAssertion interface is used with the getCache method to assert the correctness of the type of keys and values used with a NamedCache instance. The method can be used to assert that a cache should use raw types. For example:

NamedCache<Object, Object> cache = session.getCache("MyCache",
 TypeAssertion.withRawTypes());

likewise when using CacheFactory,

NamedCache<Object, Object> cache = CacheFactory.getCache("MyCache",
 TypeAssertion.withRawTypes());

For stronger type safety, you can create a cache and explicitly assert the key and value types to be used by the cache. For example, to create a cache and assert that keys and values must be of type String, an application can use:

NamedCache<String, String> cache = session.getCache("MyCache",
 TypeAssertion.withTypes(String.class, String.class));

A NameCache instance is not required to adhere to the asserted type and may choose to disregard it; however, a warning message is emitted at compile-time. For example:

NamedCache cache = session.getCache("MyCache",
 TypeAssertion.withTypes(String.class, String.class));

Likewise, an application may choose to assert that raw types be used and a name cache instance may use specific types. However, in both cases, this could lead to errors if types are left unchecked. For example:

NamedCache<String, String> cache = session.getCache("MyCache",
 TypeAssertion.withRawTypes());

For the strongest type safety, specific types can also be declared as part of a cache definition in the cache configuration file. A runtime error occurs if an application attempts to use types that are different than those configured as part of the cache definition. The following examples configures a cache that only supports keys and values that are of type String.

<cache-mapping>
   <cache-name>MyCache</cache-name>
   <scheme-name>distributed</scheme-name>
   <key-type>String</key-type>
   <value-type>String</value-type>
</cache-mapping>

Lastly, an application can choose to disable explicit type checking. If type checking is disabled, then the application is responsible for ensuring type safety.

NamedCache<Object, Object> cache = session.getCache("MyCache",
   TypeAssertion.withoutTypeChecking());