Infinispan caches are very powerful and can be configured and tuned to suit the needs of most applications. Let’s look at some of the features you can enable:
Expiration
Usually the purpose of a cache is to store short-lived data that needs to be refreshed regularly. You can tell Infinispan to store an entry only for a certain period of time:
cache.put("mortal", "human", 10, TimeUnit.MINUTES);
You can also set a default expiration lifespan for all entries in a cache via the configuration:
configurationBuilder.expiration().lifespan(10, TimeUnit.MINUTES);
or in XML:
<distributed-cache name="mortaldata">
<expiration lifespan="600000" />
<!-- lifespan in milliseconds -->
</distributed-cache>
Eviction
Normally, caches are unbounded, i.e. they grow indefinitely and it is up to the application to remove unneeded data. In Infinispan you can also set a maximum size for a cache: when this limit is reached, entries accessed least recently will be evicted. You can set the size based on number of entries:
configurationBuilder.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(1000);
or in XML:
<distributed-cache name="boundedcache">
<eviction strategy="LIRS" type="COUNT" size="1000" />
<!-- maximum 1000 entries -->
</distributed-cache>
You can also set a maximum memory occupation. This currently only works for some "well-known" data-types (String, byte[]) or when the storeAsBinary configuration option is enabled:
configurationBuilder.eviction().strategy(EvictionStrategy.LIRS).type(EvictionType.MEMORY).size(1000000);
or in XML:
<distributed-cache name="boundedcache">
<eviction strategy="LIRS" type="MEMORY" size="1000000" />
<!-- maximum 1MB -->
</distributed-cache>
Persistence
A cache, being an in-memory data structure, will lose data when stopped. You can overcome this limitation by backing a cache with a persistent store. Depending on configuration the store will act either as a persistent copy of your in-memory data or, in combination with eviction, as an overflow for excess data that you don’t want to keep in memory. There is a multitude of available cache stores available, depending on the features you need, ranging from file-based, to JDBC, to popular NoSQL and Cloud stores like MongoDB, S3, and Cassandra. Here are some examples:
Simple write-through file-based store:
configurationBuilder.persistence().addSingleFileStore().location("/path/to/persistent/data");
or in XML:
<distributed-cache name="filecache">
<persistence>
<file-store path="/path/to/persistent/data" />
</persistence>
</distributed-cache>
Write-behind JDBC store with keys represented as strings using the internal connection pool:
configurationBuilder.persistence()
.addStore(JdbcStringBasedStoreConfigurationBuilder.class)
.connectionPool()
.connectionUrl("jdbc:postgresql://dbhost:5432/infinispan")
.username("infinispan").password("infinispan").driverClass(org.postgresql.Driver.class)
.table().tableNamePrefix("ISPN_STRING_TABLE").createOnStart(true)
.async().enabled();
or in XML:
<distributed-cache name="filecache">
<persistence>
<string-keyed-jdbc-store>
<connection-pool connection-url="jdbc:postgresql://dbhost:5432/infinispan" username="infinispan" password="infinispan" driver="org.postgresql.Driver"/>
<string-keyed-table create-on-start="true" prefix="ISPN_STRING_TABLE" />
<write-behind />
</string-keyed-jdbc-store>
</persistence>
</distributed-cache>
Transactions
Infinispan can be configured to use and participate in JTA transactions. This means that you can perform multiple operations on a cache (or multiple caches) and commit them (or roll them back) atomically. You can either use the internal (but limited) DummyTransactionManager or an external transaction manager, like the one provided by your container. Configure a transactional cache with:
configurationBuilder.transaction().transactionMode(TransactionMode.TRANSACTIONAL);
or in XML:
<distributed-cache name="transactional">
<transaction mode="NON_XA" />
</distributed-cache>
and then you interact with a cache transactionally as follows:
TransactionManager tm = cache.getAdvancedCache().getTransactionManager();
tm.begin();
int i = cache.get("k1");
cache.put("k1", i++);
tm.commit();
Queries and indexing
Infinispan supports searching of Java objects stored in the grid using powerful search APIs which complement its main Cache API. Supposing your cache contains Person objects:
public class Person {
String name;
String surname;
Date birthday;
}
you can query it by obtaining a QueryFactory:
QueryFactory qf = org.infinispan.query.Search.getQueryFactory(cache);
then use the query DSL to build the query which will retrieve all Person objects whose name starts with J born between 1970 and 1975:
org.infinispan.query.dsl.Query query = qf.from(Person.class)
.having("name").like("J%")
.and().having("birthday").between(Year.of(1970), Year.of(1975))
.toBuilder().build();
List<Person> list = query.list();
By default, queries will apply the predicates to all of the entries to find matches. You can also enable the use of Lucene indexes which will provide a significant performance boost along with a bunch of additional features.
First, enable indexing as follows:
configurationBuilder.indexing().enable().index(Index.LOCAL).autoConfig(true);
or in XML:
<distributed-cache name="indexedcache">
<indexing index="LOCAL" auto-config="true" />
</distributed-cache>
then annotate the entities you are going to store in the cache by specifying which fields to index:
@Indexed
public class Person {
@Field String name;
@Field String surname;
@Field Date birthday;
}
Events and Listeners
Infinispan offers a listener API, where clients can register for and get notified when events such as entries being modified or nodes joining/leaving the cluster take place. You don’t need to configure anything in order to be able to register events on a cache. For example, the following class defines a listener to print out some information every time a new entry is added to the cache:
@Listener
public class PrintWhenAdded {
@CacheEntryCreated
public void print(CacheEntryCreatedEvent event) {
if (!event.isPre())
System.out.println("New entry " + event.getKey() + " created in the cache");
}
}
And you register the above listener on a cache with:
cache.addListener(new PrintWhenAdded());
Listener annotations have some attributes which affect their behavior. Most events will fire both before and after the actual operation is performed. You can tune this behavior by specifying an observation attribute:
@Listener(observation = Observation.POST)
Normally, listeners are local, i.e. they will receive events for affected entries only if they are owned by the node where the listener has been registered. You can register a listener that will listen to events happening on the entire cluster by using:
@Listener(clustered = true)
but be aware that this will increase cluster traffic considerably. Note that cluster listeners only support POST observation.
Executors
You can execute arbitrary code across your cluster using the ClusterExecutor. Just submit a java.util.function.Function, which receives the local CacheManager as input and can return any value to the invoker:
manager.executor().submitConsumer(localManager -> {
String s = ... ; // Perform some meaningful computation local to each node
return s;
}, (address, value, throwable) -> {
if (throwable != null) {
log.fatal("Encountered an error while processing on node " + address, t);
} else {
log.info(“Node %s has returned %s”, address, value);
}
}).join();
The code you run doesn’t necessarily need to interact with the caches themselves.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}