Mastering Hazelcast
Mastering Hazelcast
Hazelcast
The Ultimate Hazelcast Book
Current to Hazelcast 3.5
PETER VEENTJER
Foreword by Greg Luck
Mastering Hazelcast
Table of Contents
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
What is Hazelcast? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Who should read this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
What is in this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Online Book Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Online Hazelcast Resources. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1. Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1. Installing Hazelcast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2. Hazelcast with Maven or Gradle. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3. Download Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4. Building Hazelcast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5. What is next . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2. Learning The Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1. Configuring Hazelcast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.1. Configuring Hazelcast using XML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.2. Configuring for Multicast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.3. Resolving Hazelcast Configuration Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.4. Loading Hazelcast XML Configuration from Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.1.5. Loading Configuration Programmatically . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.1.6. Fluent Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.7. No Static Default HazelcastInstance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.8. Same Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.9. Wildcard Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.10. Avoiding Ambiguous Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.11. Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.12. Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.13. Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.1.14. Composing Declarative Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2. Multiple Hazelcast Instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3. Loading a DistributedObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4. Unique Names for Distributed Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.5. Reloading a DistributedObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.6. Destroying a DistributedObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7. Controlled Partitioning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Acknowledgments
Special thanks go to the Hazelcast guys: Talip Ozturk, Fuad Malikov and Enes Akar who are technically
responsible for Hazelcast and helped to answer my questions. But I really want to thank Mehmet
Dogan, architect at Hazelcast, since he was my main source of information and put up with the zillion
questions I have asked.
Also thanks to all committers and mailing list members for contributing to making Hazelcast such a
great product.
Finally, Im very grateful for my girlfriend, Ralitsa Spasova, for being a positive influence around me
and making me a better person.
Foreword
Peter Veentjer leads the QuSP (Quality, Stability and Performance) team at Hazelcast. In that role, he
roves over the whole code base with an eagle eye and has built up deep expertise on Hazelcast. Peter is
also a great communicator, wishing to spread his knowledge of and enthusiasm for Hazelcast to our
user base. So it was natural for Peter to create Mastering Hazelcast.
In Mastering Hazelcast, Peter takes an in-depth look at fundamental Hazelcast topics. This book should
be seen as a companion to the Reference Manual. The reference manual covers all Hazelcast features.
Mastering Hazelcast gives deeper coverage over the most important topics. Each chapter has a Good to
Know section, which highlights important concerns.
This
book
includes
many
code
examples.
These
and
more
can
be
accessed
from
Preface
Writing concurrent systems has long been a passion of mine, so it is a logical step to go from
concurrency control within a single JVM to concurrency control over multiple JVMs. A lot of the
knowledge that is applicable to concurrency control in a single JVM also applies to concurrency over
multiple JVMs. However, there is a whole new dimension of problems that make distributed systems
even more interesting to deal with.
What is Hazelcast?
When you professionally write applications for the JVM, you will likely write server-side applications.
Although Java has support for writing desktop applications, the server-side is where Java really shines.
Today, in the era of cloud computing, it is important that server-side systems are:
1. Scalable: just add and remove machines to match the required capacity.
2. Highly available: if one or more machines has failed, the system should continue as if nothing
happened.
3. Highly performant: performance should be fast, and cost effective.
Hazelcast is an In-Memory Data Grid that is:
1. highly available.
It does not lose data after a JVM crash because it automatically replicates partition data to other
cluster members. In the case of a member going down, the system will automatically failover by
restoring the backup. Hazelcast has no master member that can form a single point of failure; each
member has equal responsibilities.
2. lightning-fast.
Each Hazelcast member can do thousands of operations per second.
Hazelcast on its own is elastic, but not automatically elastic; it will not automatically spawn additional
JVMs to become members in the cluster when the load exceeds a certain upper threshold. Also,
Hazelcast will not shutdown JVMs when the load drops below a specific threshold. You can achieve this
by adding a glue code between Hazelcast and your cloud environment.
One of the things I like most about Hazelcast is that it is unobtrusive; as a developer/architect, you are
in control of how much Hazelcast you get in your system. You are not forced to mutilate objects so they
can be distributed, use specific application servers, complex APIs, or install software; just add the
hazelcast.jar to your classpath and you are done.
This freedom, combined with very well thought out APIs, makes Hazelcast a joy to use. In many cases,
you simply use interfaces from java.util.concurrent, such as Executor, BlockingQueue or Map. In little
time and with simple and elegant code, you can write a highly available, scalable and high-performing
system.
In Chapter 8: Hazelcast Clients, you will learn about setting up Hazelcast clients.
In Chapter 9: Serialization, you will learn more about the different serialization technologies that are
supported by Hazelcast. Java Serializable and Externalizable interfaces, and also the native Hazelcast
serialization techniques like DataSerializable and the new Portable functionality will be explained.
In Chapter 10: Transactions, you will learn about Hazelcasts transaction support, which prevents
transactional data-structures from being left in an inconsistent state.
In Chapter 11: Network Configuration, you will learn about Hazelcasts network configuration.
Different member discovery mechanisms like multicast, Amazon EC2, and security will be explained.
In Chapter 12: SPI, you will learn about using the Hazelcast SPI to make first class distributed services.
This functionality is perhaps the most important new feature introduced with Hazelcast 3.0.
In Chapter 13: Threading Model, you will learn about using the Hazelcast threading model. This
helps you write an efficient system without causing cluster stability issues.
In Chapter 14: Performance Tips, you will learn some tips to improve Hazelcast performance.
<dependencies>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>3.5</version>
</dependency>
</dependencies>
That is it. Make sure that you check the Hazelcast website to have <version> use the most recent
version number. After this dependency is added, Maven will automatically download the
dependencies needed.
To do same with Gradle, include the following to the dependencies section of your build.gradle:
dependencies {
compile "com.hazelcast:hazelcast:3.5"
}
The latest snapshot is even more recent because it is updated as soon as a change is merged in the Git
repository.
If you want to use the latest snapshot, you need to add the snapshot repository to your pom:
<repositories>
<repository>
<id>snapshot-repository</id>
<name>Maven2 Snapshot Repository</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
Or, to your build.gradle:
repositories {
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots'
}
}
Using a snapshot can be useful if you need to work with the latest and greatest.
However, the snapshot versions might contain some bugs.
can
access
the
examples
used
in
the
book
at
the
following
website:
https://github.com/hazelcast/hazelcast-code-samples.
If you want to clone the Git repository, just execute the following command:
If you have a change that you want to offer to the Hazelcast team, you commit and push your change
to your own forked repository and you create a pull request that will be reviewed by the Hazelcast
team. Once your pull request is verified, it will be merged and a new snapshot will automatically
appear in the Hazelcast snapshot repository.
<hazelcast
xsi:schemaLocation="http://www.hazelcast.com/schema/config
http://www.hazelcast.com/schema/config/hazelcast-config-3.5.xsd"
xmlns="http://www.hazelcast.com/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</hazelcast>
This configuration file example imports an XML schema (XSD) for validation. If you are using a
modern IDE (like IntelliJ IDEA), you get code completion for XML tags. To reduce the size of the
examples in the book, only the elements inside the <hazelcast> tags are listed. In the example code for
this book, you can find the full XML configuration.
<network>
<join><multicast enabled="true"/></join>
</network>
See Multicast if multicast does not work or you want to know more about it. If you are using the
programmatic configuration, then multicast is enabled by default.
String s = "<hazelcast>....</hazelcast>"
Config config = new InMemoryXmlConfig(s);
HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
UrlXmlConfig class loads the config from a URL pointing to a XML file.
<map name="testmap">
<time-to-live-seconds>10</time-to-live-seconds>
</map>
But, what if we want to create multiple map instances using the same configuration? Do we need to
configure them individually? This is impossible to do if you have a dynamic number of distributed
data structures and you do not know up front how many need to be created. The solution to this
problem is wildcard configuration, which is available for all data structures. Wildcard configuration
makes it possible to use the same configuration for multiple instances. For example, we could
configure the previous testmap example with a value of 10 for time-to-live-seconds using a wildcard
configuration like this:
<map name="testmap*">
<time-to-live-seconds>10</time-to-live-seconds>
</map>
By using a single asterisk (*) character any place in the name, the same configuration can be shared by
different data structures. The wildcard configuration can be used like this:
<map name="m*">
<time-to-live-seconds>10</time-to-live-seconds>
</map>
<map name="ma*">
<time-to-live-seconds>10</time-to-live-seconds>
</map>
If a map is loaded using hz.getMap("map") then Hazelcast will not throw an error or log a warning;
instead, Hazelcast selects one of the maps. The selection does not depend on the definition order in the
configuration file and it is not based on the best-fitting match. You should make sure that your
wildcard configurations are very specific. One of the ways to achieve this is to include the package
<map name="com.foo.testmap*">
<time-to-live-seconds>10</time-to-live-seconds>
</map>
A map can be loaded by calling Map map = hi.getMap("com.foo.testmap1").
2.1.11. Properties
Hazelcast provides an option to configure certain properties which are not part of an explicit
configuration section, such as the Map. This can be done using the properties section.
<properties>
<property name="hazelcast.icmp.enabled">true</property>
</properties>
For a full listing of available properties, see the System Properties section in the Hazelcast Reference
Manual or have a look at the GroupProperties class.
Apart from properties in the hazelcast.xml, you can also pass properties using the command line java
-Dproperty-name=property-value. One thing to watch out for is that you cannot override properties in
the hazelcast.xml or the programmatic configuration from the command line because the command
line has a lower priority.
Properties are not shared between members, so you cannot put properties in one member and read
them from another. You need to use a distributed map for that.
2.1.12. Logging
Hazelcast supports various logging mechanisms; jdk, log4, sl4j or none if you do not want to have any
logging. The default is jdk, the logging library that is part of the JRE, so no additional dependencies are
needed. You can set logging by adding a property in the hazelcast.xml:
<properties>
<property name="hazelcast.logging.type">log4j</property>
</properties>
Or, you can set with the programmatic configuration as shown below.
You can also configure it from the command line using java -Dhazelcast.logging.type=log4j. If you are
going to use log4j or slf4j, make sure that the correct dependencies are included in the classpath. See
the example sources for more information.
If you are not satisfied with the provided logging implementations, you can always implement your
own logging by using the LogListener interface. See the Logging Configuration section in the Hazelcast
Reference Manual for more information.
If you are not making use of configuring logging from the command line, be very
careful about touching Hazelcast classes. It could be that they default to the jdk
logging before the actual configured logging is read. Once the logging mechanism is
selected, it will not change. Some users make use of the command line version
instead of the properties section for logging to avoid confusion.
If you are making use of jdk logging and you are annoyed that your log entry is spread over two lines,
have a look at the SimpleLogFormatter as shown below.
java.util.logging.SimpleFormatter.format='%4$s: %5$s%6$s%n'
2.1.13. Variables
One of the new features of Hazelcast 3 is the ability to specify variables in the Hazelcast XML
configuration file. This makes it a lot easier to share the same Hazelcast configuration between
different environments and it also makes it easier to tune settings.
Variables can be used as shown below.
<executor-service name="exec">
<pool-size>${pool.size}</pool-size>
</executor-service>
In this example, the pool-size is configurable using the pool.size variable. In a production
environment, you might want to increase the pool size since you have beefier machines there. In a
development environment, you might want to set it to a low value.
By default, Hazelcast uses the system properties to replace variables with their actual value. To pass
this system property, you could add the following on the command line: -Dpool.size=1. If a variable is
not found, a log warning will be displayed but the value will not be replaced.
You can use a different mechanism than the system properties, such as a property file or a database.
You can do this by explicitly setting the Properties object on the XmlConfigBuilder as shown below.
<hazelcast>
<group>
<name>dev</name>
<password>dev-pass</password>
</group>
</hazelcast>
development-network-config.xml
<hazelcast>
<network>
<port auto-increment="true" port-count="100">5701</port>
<join>
<multicast enabled="true">
<multicast-group>224.2.2.3</multicast-group>
<multicast-port>54327</multicast-port>
</multicast>
</join>
</network>
</hazelcast>
To get your example Hazelcast declarative configuration out of the above two, use the <import/>
element as shown below.
<hazelcast>
<import resource="development-group-config.xml"/>
<import resource="development-network-config.xml"/>
</hazelcast>
This feature also applies to the declarative configuration of Hazelcast Client.
client-group-config.xml
<hazelcast-client>
<group>
<name>dev</name>
<password>dev-pass</password>
</group>
</hazelcast-client>
client-network-config.xml
<hazelcast-client>
<network>
<cluster-members>
<address>127.0.0.1:7000</address>
</cluster-members>
</network>
</hazelcast-client>
To get a Hazelcast Client declarative configuration from the above two examples, use the <import/>
element as shown below.
<hazelcast-client>
<import resource="client-group-config.xml"/>
<import resource="client-network-config.xml"/>
</hazelcast-client>
You need to use the <import/> element on the top level of the XML hierarchy.
<hazelcast>
<import resource="file:///etc/hazelcast/development-group-config.xml"/> <!-- loaded
from filesystem -->
<import resource="classpath:development-network-config.xml"/> <!-- loaded from
classpath -->
</hazelcast>
You can use property placeholders in the <import/> elements.
<hazelcast>
<import resource="${environment}-group-config.xml"/>
<import resource="${environment}-network-config.xml"/>
</hazelcast>
Members [2] {
Member [192.168.1.100]:5701 this
Member [192.168.1.100]:5702
}
And an output similar to the following in the other member.
Members [2] {
Member [192.168.1.100]:5701
Member [192.168.1.100]:5702 this
}
As you can see in the above outputs, the created cluster has 2 members.
<queue name="q"/>
And the queue can be loaded as shown below.
custom
distributed
objects
using
the
SPI,
you
can
use
the
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IdGenerator idGenerator = hz.getIdGenerator("idGenerator");
IMap someMap = hz.getMap("somemap-"+idGenerator.newId());
This technique can be used with wildcard configuration to create similar objects using a single
definition. See Wildcard Configuration.
A distributed object created with a unique name often needs to be shared between members. You can
do this by passing the ID to the other members and you can use one of the HazelcastInstance.get
methods to retrieve the DistributedObject. For more information, see Serialization: DistributedObject.
In Hazelcast, the name and type of the DistributedObject uniquely identifies that object:
releases all resources for this object within the cluster. But, you should use this method with care
because once the destroy method is called and the resources are released, a subsequent load with the
same ID from the HazelcastInstance will result in a new data structure and will not lead to an
exception.
A similar issue happens to references. If a reference to a DistributedObject is used after the
DistributedObject is destroyed, new resources will be created. In the following case, we create a cluster
with two members and each member gets a reference to the queue q. First, we place an item in the
queue. When the queue is destroyed by the first member (q1) and q2 is accessed, a new queue will be
created.
q1.size: 1 q2.size:1
q1.size: 0 q2.size:0
The system will not report any error and will behave as if nothing has happened. The only difference is
the creation of the new queue resource. Again, a lot of care needs to be taken when destroying
distributed objects.
use the name to determine the partition. The problem is that you sometimes want to control the
partition without depending on the name. Assume that you have the following two semaphores.
ISemaphore s1 = hz.getSemaphore("s1");
ISemaphore s2 = hz.getSemaphore("s2");
They would end up in different partitions because they have different names. Luckily, Hazelcast
provides a solution for that using the @ symbol, as in the following example.
ISemaphore s1 = hz.getSemaphore("s1@foo");
ISemaphore s2 = hz.getSemaphore("s2@foo");
Now, s1 and s2 will end up in the same partition because they share the same partition key: foo. This
partition key can be used to control the partition of distributed objects and can also be used to send a
Runnable to the correct member using the IExecutor.executeOnKeyOwner method, as in Distributed
Executor Service: Executing on Key Owner, and to control in which partition a map entry is stored, as
in (see Map: Partition Control).
<semaphore name="s1">
<initial-permits>3</initial-permits>
</semaphore>
This means that you can safely combine explicit partition keys with normal configuration. It is
important to understand that the name of the DistributedObject will contain the @partition-key
section. Therefore, the following two semaphores are different.
ISemaphore s1 = hz.getSemaphore("s1@foo");
ISemaphore s2 = hz.getSemaphore("s1");
Hazelcast config is not updatable: Once a HazelcastInstance is created, the Config that was used to
create that HazelcastInstance should not be updated. A lot of the internal configuration objects
are not thread-safe and there is no guarantee that a property is going to be read after it has been
read for the first time.
HazelcastInstance.shutdown(): If you are not using your HazelcastInstance anymore, make sure
to shut it down by calling the shutdown() method on the HazelcastInstance. This will release all its
resources and end network communication.
Hazelcast.shutdownAll(): This method is very practical for testing purposes if you do not have
control over the creation of Hazelcast instances, but you want to make sure that all instances are
being destroyed.
What happened to the Hazelcast.getDefaultInstance: If you have been using Hazelcast 2.x, you
might wonder what happened to the static methods like Hazelcast.getDefaultInstance and
Hazelcast.getSomeStructure. These methods have been dropped because they relied on a
singleton HazelcastInstance and when that was combined with explicit HazelcastInstances, it
caused confusion. In Hazelcast 3, it is only possible to work with an explicit HazelcastInstance.
3.1. IAtomicLong
The IAtomicLong, formally known as the AtomicNumber, is the distributed version of the
java.util.concurrent.atomic.AtomicLong, so if you have used that before, working with the IAtomicLong
should feel very similar. The IAtomicLong exposes most of the operations the AtomicLong provides, such
as get, set, getAndSet, compareAndSet and incrementAndGet. There is a big difference in performance since
remote calls are involved.
This example demonstrates the IAtomicLong by creating an instance and incrementing it one million
times:
At: 0
At: 500000
Count is 1000000
If you run multiple instances of this member, then the total count should be equal to one million times
the number of members you have started.
If the IAtomicLong becomes a contention point in your system, you have a few ways to deal with it
depending on your requirements. You can create a stripe (essentially an array) of IAtomicLong instances
to reduce pressure. Or you can keep changes local and only publish them to the IAtomicLong once a
while. There are a few downsides; you could lose information if a member goes down and the newest
value is not always immediately visible to the outside world.
3.1.1. Functions
Since Hazelcast 3.2, it is possible to send a function to an IAtomicLong. The Function class is a single
method interface: it is a part of the Hazelcast codebase since we cant yet have a dependency on Java 8.
An example of a function implementation is the following function which adds 2 to the original value:
apply.result:3
apply.value:1
alter.value:3
alterAndGet.result:3
alterAndGet.value:3
getAndAlter.result:1
getAndAlter.value:3
You might ask yourself, why not do the following approach to double an IAtomicLong?
atomicLong.set(atomicLong.get()+2));
This requires a lot less code. The biggest problem here is that this code has a race problem; the read
and the write of the IAtomicLong are not atomic, so they could be interleaved with other operations. If
you have experience with the AtomicLong from Java, then you probably have some experience with the
compareAndSet method where you can create an atomic read and write:
for(;;){
long oldValue = atomicLong.get();
long newValue = oldValue+2;
if(atomicLong.compareAndSet(oldValue,newValue)){
break;
}
}
The problem here is that the AtomicLong could be on a remote machine and therefore get and
compareAndSet are remote operations. With the function approach, you send the code to the data
instead of pulling the data to the code, making this a lot more scalable.
3.2. IdGenerator
In the previous section, the IAtomicLong was introduced. IAtomicLong can be used to generate unique
IDs within a cluster. Although that will work, it probably isnt the most scalable solution since all
members will content on incrementing the value. If you are only interested in unique IDs, you can
have a look at the com.hazelcast.core.IdGenerator.
The way the IdGenerator works is that each member claims a segment of 1 million IDs to generate. This
is done behind the scenes by using an IAtomicLong. A segment is claimed by incrementing that
IAtomicLong by 10000. After claiming the segment, the IdGenerator can increment a local counter. Once
all IDs in the segment are used, it will claim a new segment. The consequence of this approach is that
only 1 in 10000 times is network traffic needed; 9999 out of 10000, the ID generation can be done in
memory and therefore is extremely fast. Another consequence is that this approach scales a lot better
than an IAtomicLong because there is a lot less contention: 1 out of 10000 instead of 1 out of 1.
Lets see the IdGenerator in action:
3.3. IAtomicReference
In the first section of this chapter, the IAtomicLong was introduced. The IAtomicLong is very useful if you
need to deal with a long, but in some cases you need to deal with a reference. That is why Hazelcast
also
supports
the
IAtomicReference,
which
is
the
distributed
version
of
the
java.util.concurrent.atomic.AtomicReference.
Lets see the IAtomicReference in action:
foo
Just like the IAtomicLong, the IAtomicReference has methods that accept a function as argument, such as
alter, alterAndGet, getAndAlter and apply. There are big advantages for using these methods.
From a performance point of view, it is better to send the function to the data then the data to
the function. Often the function is a lot smaller than the value and therefore the function is
cheaper to send over the line. Also, the function only needs to be transferred once to the
target machine, while the value needs to be transferred twice.
You dont need to deal with concurrency control. If you would do a load, transform, and
store, you could run into a data race since another thread might have updated the value you
are about to overwrite.
When a function is executed on the AtomicReference, make sure that the function doesnt run
too long. As long as that function is running, the whole partition is not able to execute other
requests. Dont hog the operation thread.
Some issues you need to be aware of:
The IAtomicReference works based on byte-content, not on object-reference. Therefore, if you
are using the compareAndSet method, it is important that you do not change to the original
value because its serialized content will then be different. It is also important to know that if
you rely on Java serialization, sometimes (especially with hashmaps) the same object can
result in different binary content.
The IAtomicReference will always have 1 synchronous backup.
All methods returning an object will return a private copy. You can modify it, but the rest of
the world will be shielded from your changes. If you want these changes to be visible to the
rest of the world, you need to write the change back to the IAtomicReference; but be careful
with introducing a data race.
The in-memory format of an IAtomicReference is binary. So the receiving side doesnt need to
have the class definition available, unless it needs to be deserialized on the other side (for
example, because a method like alter is executed). This deserialization is done for every call
that needs to have the object instead of the binary content, so be careful with expensive
object graphs that need to be deserialized.
If you have an object graph or an object with many fields, and you only need to calculate
some information or you only need a subset of fields, you can use the apply method. This way,
the whole object doesnt need to be sent over the line, only the information that is relevant.
3.4. ILock
A lock is a synchronization primitive that makes it possible for only a single thread to access a critical
section of code; if multiple threads at the same moment were accessing that critical section, you would
get race problems.
Hazelcast provides a distributed lock implementation and makes it possible to create a critical section
within a cluster of JVMs, so only a single thread from one of the JVMs in the cluster is allowed to
acquire that lock. Other threads, no matter if they are on the same JVMs or not, will not be able to
acquire the lock; depending on the locking method they called, they either block or fail. The
com.hazelcast.core.ILock extends the java.util.concurrent.locks.Lock interface, so using the lock is
quite simple.
The following example shows how a lock can be used to solve a race problem:
lock.lock();
try{
...do your stuff.
}finally{
lock.unlock();
}
It is important that the lock is acquired before the try/finally block is entered. So, the following
example is not good.
try{
lock.lock();
...do your stuff.
}finally{
lock.unlock();
}
In case of Hazelcast, it can happen that the lock is not granted because the lock method has a timeout
of 5 minutes. If this happens, an exception is thrown, the finally block is executed, and the lock.unlock
is called. Hazelcast will see that the lock is not acquired and an IllegalMonitorStateException with the
message "Current thread is not owner of the lock!" is thrown. In case of a tryLock with a timeout, the
following idiom is recommended:
if(!lock.tryLock(timeout, timeunit)){
throw new RuntimeException();
}
try{
...do your stuff.
}finally{
lock.unlock();
}
The tryLock is acquired outside of the try/finally block. In this case, an exception is thrown if the lock
cant be acquired within the given timeout, but another flow that prevents entering the try/finally
block also is valid.
Here are more general issues worth knowing about the Hazelcast lock.
Hazelcast lock is reentrant, so you can acquire it multiple times in a single thread without causing a
deadlock. Of course, you need to release it as many times as you have acquired it to make it
available to other threads.
As with the other Lock implementations, Hazelcast lock should always be acquired outside of a
try/finally block. Otherwise, the lock acquire can fail, but an unlock is still executed.
Keep locks as short as possible. If locks are kept too long, it can lead to performance problems, or
worse, deadlock.
With locks it is easy to run into deadlocks. Having code you dont control or understand running
inside your locks is asking for problems. Make sure you understand exactly the scope of the lock.
To reduce the chance of a deadlock, you can use the Lock.tryLock method to control the waiting
period. The lock.lock() method will not block indefinitely, but will timeout with an
OperationTimeoutException after 300 seconds.
Locks are automatically released when a member has acquired a lock and that member goes down.
This prevents threads that are waiting for a lock from waiting indefinitely. This is also needed for
failover to work in a distributed system. The downside is that if a member goes down that acquired
the lock and started to make changes, other members could start to see partial changes. In these
cases, either the system could do some self repair or a transaction might solve the problem.
A lock must always be released by the same thread that acquired it, otherwise look at the
ISemaphore.
Locks are fair, so they will be granted in the order they are requested.
There are no configuration options available for the lock.
A lock can be checked if it is locked using the ILock.isLocked method, although the value could be
stale as soon as it is returned.
A lock can be forced to unlock using the ILock.forceUnlock() method. It should be used with
extreme care since it could break a critical section.
The Hazelcast.getLock doesnt work on a name of type String, but can be a key of any type. This key
will be serialized and the byte array content determines the actual lock to acquire. So, if you are
passing in an object as key, it isnt the monitor lock of that object that is being acquired.
Replication: the ILock has one synchronous backup and zero asynchronous backups and is not
configurable.
A lock is not automatically garbage collected. So if you create new locks over time, make sure to
destroy them. If you dont, you can run into an OutOfMemoryError.
3.5. ICondition
With a Condition, it is possible to wait for certain conditions to happen: for example, wait for an item
to be placed on a queue. Each lock can have multiple conditions, such as if an item is available in the
queue and if room is available in the queue. In Hazelcast 3, the ICondition, which extends the
java.util.concurrent.locks.Condition, has been added.
There is one difference: with the normal Java version, you create a condition using the
Lock.newCondition() method. Unfortunately, this doesnt work in a distributed environment since
Hazelcast has no way of knowing if Conditions created on different members are the same Condition
or not. You dont want to rely on the order of their creation, so in Hazelcast, a Condition needs to be
created using the ILock.newCondition(String name) method.
In the following example, we are going to create one member that waits for a counter to have a certain
value. Another member will set the value on that counter. Lets get started with the waiting member:
Waiting
The next part will be the NotifyMember. Here, the Lock is acquired, the value is set to 1, and the
isOneCondition will be signaled:
Waiting
Wait finished, counter: 1
3.6. ISemaphore
The semaphore is a classic synchronization aid that can be used to control the number of threads
doing a certain activity concurrently, such as using a resource. Each semaphore has a number of
permits, where each permit represents a single thread allowed to execute that activity concurrently. As
soon as a thread wants to start with the activity, it takes a permit (or waits until one becomes available)
and once finished with the activity, the permit is returned.
If you initialize the semaphore with a single permit, it will look a lot like a lock. A big difference is that
the semaphore has no concept of ownership. With a lock, the thread that acquired the lock must
release it, but with a semaphore, any thread can release an acquired permit. Another difference is that
an exclusive lock only has 1 permit, while a semaphore can have more than 1.
Hazelcast
provides
distributed
version
of
the
java.util.concurrent.Semaphore
named
as
the
thread
is
interrupted,
or
when
the
semaphore
is
destroyed
and
an
InstanceDestroyedException is thrown.
The following example explains the semaphore. To simulate a shared resource, we have an IAtomicLong
initialized with the value 0. This resource is going to be used 1000 times. When a thread starts to use
that resource, the resource will be incremented, and when finished it will be decremented.
<semaphore name="semaphore">
<initial-permits>3</initial-permits>
</semaphore>
When you start the SemaphoreMember 5 times, you will see the output like this:
At
At
At
At
At
iteration:
iteration:
iteration:
iteration:
iteration:
0,
1,
2,
3,
4,
Active
Active
Active
Active
Active
Threads:
Threads:
Threads:
Threads:
Threads:
1
2
3
3
3
The maximum number of concurrent threads using that resource is always equal to or smaller than 3.
As an experiment, you can remove the semaphore acquire/release statements and see for yourself in
the output that there is no longer control on the number of concurrent usages of the resources.
3.6.1. Replication
Hazelcast provides replication support for the ISemaphore: if a member goes and replication is enabled
(by default it is), then another member takes over the semaphore without permit information getting
lost. This can be done by synchronous and asynchronous replication, which can be configured using
the backup-count and async-backup-count properties:
backup-count: Number of synchronous replicas and defaults to 1.
async-backup-count: Number of asynchronous replicas and defaults to 0.
If high performance is more important than permit information getting lost, you might consider
setting backup-count to 0.
Good to know
A few things worth knowing about the ISemaphore:
Fairness. The ISemaphore acquire methods are fair and this is not configurable. So under
contention, the longest waiting thread for a permit will acquire it before all other threads.
This is done to prevent starvation, at the expense of reduced throughput.
Automatic permit release. One of the features of the ISemaphore to make it more reliable in a
distributed environment is the automatic release of a permit when the member fails (similar
to the Hazelcast Lock). If the permit would not be released, the system could run in a
deadlock.
The acquire() method doesnt timeout, unlike the Hazelcast Lock.lock() method. To prevent
running
into
deadlock,
you
can
use
one
of
timed
acquire
methods,
like
3.7. ICountDownLatch
The java.util.concurrent.CountDownLatch was introduced in Java 1.5 and is a synchronization aid that
makes it possible for threads to wait until a set of operations that are being performed by one or more
threads are completed. A CountDownLatch can be seen as a gate containing a counter. Behind this gate,
threads can wait till the counter reaches 0. CountDownLatches often are used when you have some
kind of processing operation, and one or more threads need to wait till this operation completes so
they
can
execute
their
logic.
Hazelcast
also
contains
CountDownLatch:
the
com.hazelcast.core.ICountDownLatch.
To explain the ICountDownLatch, imagine that there is a leader process that is executing some action that
will eventually complete. Also imagine that there are one or more follower processes that need to do
something after the leader has completed. We can implement the behavior of the Leader:
distributed environment, the leader could go down before it reaches zero and this would result in the
waiters waiting till the end of time. Because this behavior is undesirable, Hazelcast will automatically
notify all listeners if the owner gets disconnected, and therefore listeners could be notified before all
steps of a certain process are completed. To deal with this situation, the current state of the process
needs to be verified and appropriate actions need to be taken: for example, restart all operations,
continue with the first failed operation, or throw an exception.
Although the ICountDownLatch is a very useful synchronization aid, it probably isnt the one you will use
on a daily basis. Unlike Javas implementation, Hazelcasts ICountDownLatch count can be reset after a
countdown has finished, but it cannot be reset during an active count.
Replication: the ICountDownLatch has 1 synchronous backup and zero asynchronous backups and is not
configurable.
4.1. IQueue
A BlockingQueue is one of the work horses for concurrent system because it allows producers and
consumers of messages (which can be POJOs) to work at different speeds. The Hazelcast
com.hazelcast.core.IQueue, which extends the java.util.concurrent.BlockingQueue, allows threads from
the same JVM to interact with that queue. Since the queue is distributed, it also allows different JVMs to
interact with it. You can add items in one JVM and remove them in another.
As an example, well create a producer/consumer implementation that is connected by a distributed
queue. The producer is going to put a total of 100 Integers on the queue with a rate of 1
message/second.
Produced 1
Produced 2
....
When you start a single consumer, you will see the following output:
Consumed 1
Consumed 2
....
As you can see, the items produced on the queue by the producer are being consumed from that same
queue by the consumer.
Because messages are produced 5 times faster than they are consumed, the queue will keep growing
with a single consumer. To improve throughput, you can start more consumers. If we start another
one, well see each consumer takes care of half the messages.
Consumer 1:
Consumed 20
Consumed 22
....
Consumer 2:
Consumed 21
Consumed 23
....
When you kill one of the consumers, the remaining consumer will process all the elements again:
Consumed 40
Consumed 42
....
If there are many producers/consumers interacting with the queue, there will be a lot of contention
and eventually the queue will become a bottleneck. One way you can solve this is to introduce a stripe
(essentially a list) of queues. But if you do, the ordering of messages sent to different queues will no
longer be guaranteed. In many cases, a strict ordering isnt required and a stripe can be a simple
solution to improve scalability.
Although the Hazelcast distributed queue preserves ordering of the messages (the
messages are taken from the queue in the same order they were put on the queue), if
there are multiple consumers, the processing order is not guaranteed because the
queue will not provide any ordering guarantees on the messages after they are taken
from the queue.
4.1.1. Capacity
In the previous example, we showed a basic producer/consumer solution based on a distributed queue.
Because the production of messages is separated from the consumption of messages, the speed of
production is not influenced by the speed of consumption. If producing messages goes quicker than the
consumption, then the queue will increase in size. If there is no bound on the capacity of the queue,
then machines can run out of memory and you will get an OutOfMemoryError.
With the traditional BlockingQueue implementation, such as the LinkedBlockingQueue, you can set a
capacity. When this is set and the maximum capacity is reached, placement of new items either fails or
blocks, depending on the type of the put operation. This prevents the queue from growing beyond a
healthy capacity and the JVM from failing. It is important to understand that the IQueue is not a
partitioned data structure like the IMap, so the content of the IQueue will not be spread over the
members in the cluster. A single member in the cluster will be responsible for keeping the complete
content of the IQueue in memory. Depending on the configuration, there will also be a backup which
keeps the whole queue in the memory.
The Hazelcast queue also provides capacity control, but instead of having a fixed capacity for the
whole cluster, Hazelcast provides a scalable capacity by setting the queue capacity using the queue
property max-size.
<network>
<join><multicast enabled="true"/></join>
</network>
<queue name="queue">
<max-size>10</max-size>
</queue>
When we start a single producer, well see that 10 items are put on the queue and then the producer
blocks. If we then start a single consumer, well see that the messages are being consumed and the
producer will produce again.
4.1.2. Backups
By default, Hazelcast will make sure that there is one synchronous backup for the queue. If the
member hosting that queue fails, the backups on another member will be used so no entries are lost.
Backups can be controlled using the following properties.
backup-count: Number of synchronous backups, defaults to 1. So by default, no entries will be lost if
a member fails.
async-backup-count: Number of asynchronous backups, defaults to 0.
If you want increased high availability, you can either increase the backup-count or the async-backupcount. If you want to have improved performance, you can set the backup-count to 0, but at the cost of
potentially losing entries on failure.
4.1.3. QueueStore
By default, Hazelcast data structures like the IQueue are not persistent.
If the cluster starts, the queues will not be populated by themselves.
Changes in the queue will not be made persistent, so if the cluster fails, then entries will be lost.
In some cases, this behavior is not desirable. Luckily, Hazelcast provides a mechanism for queue
durability using the QueueStore, which can connect to a more durable storage mechanism, such as a
database. In Hazelcast 2, the Queue was implemented on top of the Hazelcast Map, so in theory you
could make the queue persistent by configuring the MapStore of the backing map. In Hazelcast 3, the
Queue is not implemented on top of a map; instead, it exposes a QueueStore directly.
4.2. IList
A List is a collection where every element only occurs once and where the order of the elements does
matter. The Hazelcast com.hazelcast.core.IList implements the java.util.List. Well demonstrate the
IList by adding items to a list on one member and printing the element of that list on another
member:
Tokyo
Paris
New York
Reading finished!
The data that the WriteMember writes to the List is visible in the ReadMember and the order is maintained.
The List interface has various methods (like the sublist) that returns collections, but it is important to
understand that the returned collections are snapshots and are not backed up by the list. See Iterator
Stability for a discussion of weak consistency.
4.3. ISet
A Set is a collection where every element only occurs once and where the order of the elements doesnt
matter. The Hazelcast com.hazelcast.core.ISet implements the java.util.Set. Well demonstrate the
Set by adding items in a Set on one member, and printing all the elements from that Set on another
member:
Paris
Tokyo
New York
Reading finished!
As you can see, the data added by the WriteMember is visible in the ReadMember. As you also can see, the
order is not maintained since order is not defined by the Set.
Just as with normal HashSet, the hashcode() and equals() methods of the object are used and not the
equals/hash of the byte array version of that object. This is a different behavior compared to the map;
see Map: Hashcode and Equals.
In Hazelcast, the ISet (and the IList) is implemented as a collection within the MultiMap, where the ID
of the Set is the key in the MultiMap and the value is the collection. This means that the ISet is not
partitioned, so you cant scale beyond the capacity of a single machine and you cannot control the
partition where data from a Set is going to be stored. If you want to have a distributed Set that behaves
more like the distributed Map, you can implement a Set based on a Map where the value is some bogus
value. It is not possible to rely on the Map.keySet for returning a usable distributed Set, since it will
return a non-distributed snapshot of the keys.
item
item
item
item
added:foo
added:bar
removed:foo
removed:bar
ItemListeners are useful if you need to react upon changes in collections. But realize that listeners are
executed asynchronously, so it could be that at the time your listener runs, the collection has changed
again.
Ordering: All events are ordered: listeners will receive and process the events in the order they
actually occurred.
explicitly. Once an item is added to the implicit destroyed collection, the collection will
automatically be recreated.
No merge policy for the Queue: If a cluster containing a queue is split, then each subcluster will
still able to access their own view of that queue. If these subclusters merge, the queue cannot be
merged and one of them is deleted.
Not partitioned: The IList/ISet/IQueue are not partitioned, so the maximum size of the collection
doesnt rely on the size of the cluster, but on the capacity of a single member since the whole
queue will be kept in the memory of a single JVM.
This is a big difference compared to Hazelcast 2.x, where they were partitioned. The Hazelcast
team decided to drop this behavior since the 2.x implementation was not truly partitioned due to
reliance on a single member where a lot of metadata for the collection was stored. This
limitation needs to be taken into consideration when you are designing a distributed system. You
can solve this issue by using a stripe of collections or by building your collection on top of the
IMap. Another more flexible but probably more time consuming alternative is to write the
collection on top of the new SPI functionality; see SPI.
A potential solution for the IQueue is to make a stripe of queues instead of a single queue. Since
each collection in that stripe is likely to be assigned to a different partition than its neighbors, the
queues will end up in different members. If ordering of items is not important, the item can be
placed on an arbitrary queue. Otherwise, the right queue could be selected based on some
property of the item so that all items having the same property end up in the same queue.
Uncontrollable partition: It is currently not possible to control the partition the collection is going
to be placed on, so more remoting is required than is strictly needed. In the future, it will be
possible for you to say:
<map name="cities"/>
Lazy creation: The Map is not created when the getMap method is called. Only when the Map instance is
accessed, it will be created. This is useful to know if you use the DistributedObjectListener and fail to
receive creation events.
5.2. Reading/Writing
The Hazelcast Map implements the ConcurrentMap interface, so reading/writing key/values is simple
since you can use familiar methods like get and put.
To demonstrate this basic behavior, the following Member creates a Map and writes some entries into
that map:
3 New York
1 Tokyo
2 Paris
The map updates from the FillMapMember are visible in the PrintAllMember.
Internally, Hazelcast will serialize the key/values (see Serialization) to byte arrays and store them in
the underlying storage mechanism. This means changes made to a key/value after they are stored in
the Map will not be reflected on the stored state. Therefore, the following code is broken:
Employee e = employees.get(123);
e.setFired(true);
If you want this change to be stored in the Map, you need to put the updated value back:
Employee e = employees.get(123);
e.setFired(true);
employees.put(123,e);
5.3. InMemoryFormat
The IMap is a distributed data structure, so a key/value can be read/written on a different machine than
where the actual content is stored. To make this possible, Hazelcast serializes the key/value to byte
arrays when they are stored, and Hazelcast deserializes the key/value when they are loaded. A
serialized representation of an object is called the binary format. For more information about
serialization of keys/values, see Serialization.
Serializing and deserializing an object too frequently on one node can have a huge impact on
performance. A typical use case would be Queries (predicate) and Entry Processors reading the same
value multiple times. To eliminate this impact on performance, the objects should be stored in object
format, not in binary format; this means that the value returned is the instance and not a byte array.
That is why the IMap provides control on the format of the stored value using the in-memory-format
setting. This option is only available for values; keys will always be stored in binary format. You should
understand the available in-memory formats:
BINARY: the value is stored in binary format. Every time the value is needed, it will be deserialized.
OBJECT: the value is stored in object format. If a value is needed in a query/entry-processor, this
value is used and no deserialization is needed.
The default in-memory-format is BINARY.
The big question is which one to use. You should consider using the OBJECT in-memory format if the
majority of your Hazelcast usage is composed of queries/entry processors. The reason is that no
deserialization is needed when a value is used in a query/entry processor because the object already is
available in object format. With the BINARY in-memory format, a deserialization is needed since the
object is only available in binary format.
If the majority of your operations are regular Map operations like put or get, you should consider the
BINARY in-memory format. This sounds counterintuitive because normal operations, such as get, rely on
the object instance, and with a binary format no instance is available. But when the OBJECT in-memory
format is used, the Map never returns the stored instance but creates a clone instead. This involves a
serialization on the owning node followed by a deserialization on the caller node. With the BINARY
format, only a deserialization is needed and therefore the process is faster. For similar reasons, a put
with the BINARY in-memory format will be faster than the OBJECT in-memory format. When the OBJECT
in-memory format is used, the Map will not store the actual instance, but will make a clone; this
involves a serialization followed by a deserialization. When the BINARY in-memory format is used, only
a deserialization is needed.
In the following example, you can see a Map configured with the OBJECT in-memory format.
<map name="cities">
<in-memory-format>OBJECT</in-memory-format>
</map>
If a value is stored in OBJECT in-memory format, a change on a returned value does not affect the stored
instance because a clone of the stored value is returned, not the actual instance. Therefore, changes
made on an object after it is returned will not be reflected on the actual stored data. Also, when a value
is written to a Map, if the value is stored in OBJECT format, it will be a copy of the put value, not the
original. Therefore, changes made on the object after it is stored will not be reflected on the actual
stored data.
normalMap.get: foo
hzMap.get: null
The Pair works fine for a HashMap, but doesnt work for a Hazelcast IMap.
For a key, it is very important that the binary format of equal objects are the same. For values, this
depends on the in-memory-format setting. If we configure the following three maps in the hazelcast.xml:
<hazelcast>
<map name="objectMap">
<in-memory-format>OBJECT</in-memory-format>
</map>
<map name="binaryMap">
<in-memory-format>BINARY</in-memory-format>
</map>
</hazelcast>
In the following code, we define two values, v1 and v2, where the resulting byte array is different. The
equals method will indicate that they are the same. We put v1 in each map and check for its existence
using map.contains(v2).
normalMap.contains:true
binaryMap.contains:false
objectMap.contains:true
v1 is found using v2 in the normalMap and the objectMap. This is because with these maps, the equals is
done based on the equals method of the object itself. But with the binaryMap, the equals is done based
on the binary format. Since v1 and v2 have different binary formats, v1 will not be found using v2.
Even though the hashcode of a key/value is not used by Hazelcast to determine the partition the
key/value will be stored in, it will be used by methods like Map.values() and Map.keySet() and therefore
it is important that the hash and equals are implemented correctly. For more information, the book
"Effective Java" mentions that you should obey the general contract when overriding equals; always
override hashcode when you override equals.
Collocating data in a single partition often needs to be combined with sending the functionality to the
partition that contains the collocated data. For example, if an invoice needs to be created for the orders
of
customer,
Callable
that
creates
the
Invoice
could
be
sent
using
the
<map name="persons">
<backup-count>1</backup-count>
</map>
You can set backup-count to 0 if you favor performance over high availability. You can specify a higher
value than 1 if you require increased availability; but the maximum number of backups is 6. The
default is 1, so in a lot of cases you dont need to specify it.
By default, the backup operations are synchronous; you are guaranteed that the backup(s) are updated
before a method call like map.put completes. But this guarantee comes at the cost of blocking and
therefore the latency increases. In some cases, having a low latency is more important than having
perfect backup guarantees, as long as the window for failure is small. That is why Hazelcast also
supports asynchronous backups, where the backups are made at some point in time. This can be
configured through the async-backup-count property:
<map name="persons">
<backup-count>0</backup-count>
<async-backup-count>1<async-backup-count>
</map>
The async-backup-count defaults to 0. Unless you want to have asynchronous backups, it doesnt need to
be configured.
Although backups can improve high availability, it will increase memory usage since the backups are
also kept in memory. So for every backup, you will double the original memory consumption.
By default, Hazelcast provides sequential consistency: when a Map entry is read, the most recent
written value is seen. This is done by routing the get request to the member that owns the key and
therefore there will be no out-of-sync copies. But sequential consistency comes at a price: if the value is
read on an arbitrary cluster member, then Hazelcast needs to do a remote call to the member that
owns the partition for that key. Hazelcast provides the option to increase performance by reducing
consistency. This is done by allowing reads to potentially see stale data. This feature is available only
when there is at least 1 backup (synchronous or asynchronous). You can enable it by setting the readbackup-data property:
<map name="persons">
<backup-count>0</backup-count>
<async-backup-count>1</async-backup-count>
<read-backup-data>true</read-backup-data>
</map>
In this example, you can see a person Map with a single asynchronous backup and reading of backup
data is enabled (the read-backup-data property defaults to false). Reading from the backup can improve
performance a bit; if you have a 10 node cluster and read-backup-data is false, there is a 1 in 10 chance
that the read will find the data locally. When there is a single backup and read-backup-data is false, that
adds another 1 in 10 chance that read will find the backup data locally. This totals to a 1 in 5 chance
that the data is found locally.
5.7. Eviction
By default, all the Map entries that are put in the Map will remain in that Map. You can delete them
manually, but you can also rely on an eviction policy that deletes items automatically. This feature
enables Hazelcast to be used as a distributed cache since hot data is kept in memory and cold data is
evicted.
The eviction configuration can be done using the following parameters:
max-size: Maximum size of the map. When maximum size is reached, the Map is evicted based on
the policy defined. The value is an integer between 0 and Integer.MAX VALUE. 0 means
Integer.MAX_VALUE and the default is 0. A policy attribute (eviction-policy seen below)
determines how the max-size will be interpreted.
PER_NODE: Maximum number of map entries in the JVM. This is the default policy.
PER_PARTITION: Maximum number of map entries within a single partition. This is probably not
a policy you will use often, because the storage size depends on the number of partitions that a
member is hosting. If the cluster is small, it will host more partitions and therefore more map
entries than with a larger cluster.
USED_HEAP_SIZE: Maximum used heap size in MB (mega-bytes) per JVM.
USED_HEAP_PERCENTAGE: Maximum used heap size as a percentage of the JVM heap size. If the JVM
is configured with 1000 MB and the max-size is 10, this policy allows the map to be 100 MB
<map name="articles">
<max-size policy="PER_NODE">10000</max-size>
<eviction-policy>LRU</eviction-policy>
<max-idle-seconds>60</max-idle-seconds>
</map>
This configures an articles map that will start to evict map entries from a member, as soon as the map
size within that member exceeds 10000. It will then start to remove map entries that are least recently
used. Also, when map entries are not used for more than 60 seconds, they will be evicted as well.
You can evict a key manually by calling the IMap.evict(key) method. You might wonder what the
difference is between this method and the IMap.delete(key). If no MapStore is defined, there is no
difference. If a MapStore is defined, an IMap.delete will call a delete on the MapStore and potentially
delete the map entry from the database. However, the evict method removes the map entry only from
the map.
MapStore.delete(Object key) is not called when a MapStore is used and a map entry is evicted. So if the
MapStore is connected to a database, no record entries are removed due to map entries being evicted.
<map name="articles">
<near-cache/>
</map>
You can configure the following properties on the near cache:
max-size: Maximum number of cache entries per local cache. As soon as the maximum size has
been reached, the cache will start to evict entries based on the eviction policy. max-size should be
between 0 and Integer.MAX_SIZE, where 0 will be interpreted as Integer.MAX_SIZE. The default is
Integer.MAX_SIZE, but it is better to either explicitly configure max-size in combination with an
eviction-policy, or set time-to-live-seconds/max-idle-seconds to prevent OutOfMemoryErrors. The
max-size of the near cache is independent of that of the map itself.
eviction-policy: Policy used to evict members from the cache when the near cache is full. The
following options are available:
NONE: No items will be evicted, so the max-size is ignored. If you want max-size to work, you need
to set an eviction-policy other than NONE. You can combine NONE with time-to-live-seconds
and max-idle-seconds.
LRU: Least Recently Used. This is the default policy.
LFU: Least Frequently Used.
time-to-live-seconds: Number of seconds a map entry is allowed to remain in the cache. Valid
values are 0 to Integer.MAX_SIZE, and 0 will be interpreted as infinite. The default is 0.
max-idle-seconds: Maximum number of seconds a map entry is allowed to stay in the cache without
being read. max-idle-seconds should be between 0 and Integer.MAX_SIZE, where 0 will be
interpreted as Integer.MAX_SIZE. The default is 0.
invalidate-on-change: If true, all the members listen for change in their cached entries and evict the
entry when it is updated or deleted. Valid values are true/false and the default is true.
in-memory-format: In-memory format of the cache. Defaults to BINARY. For more information, see
InMemoryFormat.
Here is an example configuration.
<map name="articles">
<near-cache/>
<max-size>10000</max-size>
<eviction-policy>LRU</eviction-policy>
<max-idle-seconds>60</max-idle-seconds>
</near-cache>
</map>
This configures an articles map with a near-cache. It will evict near-cache entries from a member as
soon as the near-cache size within that member exceeds 10000. It will then remove near-cache entries
that are least recently used. When near cache entries are not used for more than 60 seconds, they will
be evicted as well.
The previous Eviction section discussed evicting items from the map, but it is important to understand
that near cache and map eviction are two different things. The near cache is a local map that contains
frequently accessed map entries from any member, while the local map will only contain map entries
it owns. You can even combine the eviction and the near cache, although their settings are
independent.
Some things worth considering when using a near cache:
It increases memory usage since the near cache items need to be stored in the memory of the
member.
It reduces consistency, especially when invalidate-on-change is false: it could be that a cache entry
is never refreshed.
It is best used for read only data, especially when invalidate-on-change is enabled. There is a lot of
remoting involved to invalidate the cache entry when a map entry is updated.
It can also be enabled on the client. See Hazelcast Clients.
There is no functionality currently available to heat up the cache.
Hazelcast
map
itself
is
thread-safe,
just
like
the
ConcurrentHashMap
or
the
Collections.synchronizedMap. In some cases, your thread safety requirements are bigger than what
Hazelcast provides out of the box. Luckily, Hazelcast provides multiple concurrency control solutions;
it can either be pessimistic using locks, or optimistic using compare and swap operations. You can also
use the executeOnKey API, such as the IMap.executeOnKey method. Instead of dealing with pessimistic
locking, such as IMap.lock(key), or dealing with optimistic locking, such as IMap.replace(key, oldvalue,
newvalue), the executeOnKey takes care of concurrency control for you with very low overhead.
5.10. EntryProcessor
One of the new features of Hazelcast 3 is the EntryProcessor. It allows to send a function, the
EntryProcessor, to a particular key or to all keys in an IMap. Once the EntryProcessor is completed, it is
discarded, so it is not a durable mechanism like the EntryListener or the MapInterceptor.
Imagine that you have a map of employees, and you want to give every employee a bonus. In the
example below, you see a very naive implementation of this functionality:
That is why the EntryProcessor was added to Hazelcast. The EntryProcessor captures the logic that
should be executed on a map entry. Hazelcast will send the EntryProcessor to each member in the
cluster, and then each member will, in parallel, apply the EntryProcessor to all map entries. This means
that the EntryProcessor is scalable; the more machines you add, the faster the processing will be
completed. Another important feature of the EntryProcessor is that it will deal with race problems by
acquiring exclusive access to the map entry when it is processing.
In the following example, the raise functionality is implemented using a EntryProcessor.
log or retrieve information. The previous example, where the total salary of all employees is
calculated, is such a situation. That is why the GetSalaryEntryProcessor constructor calls the super with
false; this signals the AbstractEntryProcessor not to apply any logic to the backup, only to the primary.
To fully understand how EntryProcessor works, lets have a look at the implementation of the
AbstractEntryProcessor:
5.10.3. Threading
To understand how the EntryProcessor works, you need to understand how the threading works.
Hazelcast will only allow a single thread the partition thread to be active in a partition. This means
that by design it isnt possible that operations like IMap.put are interleaved with other map operations,
or with system operations like migration of a partition. The EntryProcessor will also be executed on the
partition thread; therefore, while the EntryProcessor is running, no other operations on that map entry
can happen.
It is important to understand that an EntryProcessor should run quickly because it is running on the
partition thread. This means that other operations on the same partition will be blocked, and that
other operations that use a different partition but are mapped to the same operation thread will also
be blocked. Also, system operations such as partition migration will be blocked by a long running
EntryProcessor. The same applies when an EntryProcessor is executed on a large number of entries; all
entries are executed in a single run and will not be interleaved with other operations.
You need to take care to store mutable states in your EntryProcessor. For example, if a member
contains partition 1 and 2 and they are mapped to partition threads 1 and 2, and if you are executing
the entry processor on map entries in partition 1 and 2, then the same EntryProcessor will be used by
different threads in parallel. It isnt a problem when you use IMap.executeOnKey, but it can be a problem
with the other IMap.execute methods.
InMemoryFormat: If you are often using the EntryProcessor or queries, it might be a good idea to
use
the
InMemoryFormat.OBJECT.
The
OBJECT
in-memory
format
in
Hazelcast
will
not
serialize/deserialize the entry, so you are able to apply the EntryProcessor without serialization
cost. The value instance that is stored is passed to the EntryProcessor, and that instance will also
be stored in the map entry (unless you create a new instance). For more information, see
InMemoryFormat.
Process single key: If you want to execute the EntryProcessor on a single key, you can use the
IMap.executeOnKey method. You could do the same with an IExecutorService.executeOnKeyOwner,
but you would need to lock and potentially deal with more serialization.
Not Threadsafe: If state is stored in the EntryProcessor between process invocations, you need to
understand that this state can be touched by different threads. This is because the same
EntryProcessor instance can be used between different partitions that run on different threads.
One potential solution is to put the state in a thread local.
Process using predicate: Deletion: You can delete items with the EntryProcessor by setting the map
entry value to null. In the following example, you can see that all bad employees are being
deleted using this approach:
class DeleteBadEmployeeEntryProcessor
extends AbstractEntryProcessor<String, Employee> {
@Override
public Object process(Map.Entry< String, Employee> entry) {
if(entry.getValue().isBad()){
entry.setValue(null);
}
return null;
}
}
HazelcastInstanceAware: Because the EntryProcessor needs to be serialized to be sent to another
machine, you cant pass it complex dependencies like the HazelcastInstance. When the
HazelcastInstanceAware interface is implemented, the dependencies can be injected. For more
information, see Serialization: HazelcastInstanceAware.
5.11. MapListener
Using one of the MapListener sub-interfaces you can listen for map entry events providing a predicate,
and so the events will be fired for each entry validated by your query. Rather than have one large
interface to handle all callback types you can just implement specific interfaces for the callback you
are interested in.
For example, if you just wish to intercept events for IMap.put you could create a listener that
implements EntryAddedListener<K,V> and EntryUpdatedListener<K,V>
IMap has a single method for applying a listener, IMap.addEntryListener. If registering the callback
inside cluster members this will cause it to fire on every member for any event. If you wish a callback
to
fire
only
when
the
event
is
local
to
that
member
you
should
register
IMap.addLocalEntryListener.
using
5.11.1. Threading
To correctly use the MapListener, you must understand the threading model. Unlike the EntryProcessor,
the MapListener doesnt run on the partition threads. It runs on an event thread, the same threads that
are used by other collection listeners and by ITopic message listeners. The MapListener is allowed to
access other partitions. Just like other logic that runs on an event thread, you need to watch out for
long running tasks because it could lead to starvation of other event listeners since they dont get a
thread. But it can also lead to OOME because of events being queued quicker than they are being
processed.
No events: When no EntryListeners are registered, no events will be sent, so you will not pay the
price for something you dont use.
HazelcastInstanceAware: When an EntryListener is sent to a different machine, it will be
serialized and then deserialized. This can be problematic if you need to access dependencies
which cant be serialized. To deal with this problem, if the EntryListener implements
HazelcastInstanceAware, you can inject the HazelcastInstance. For more information see
Serialization: HazelcastInstanceAware.
EntryListener: Prior to 3.5 Hazelcast had one interface for all Map Event callbacks, called the
EntryListener. EntryListener has been retained for backward compatibility, for new code please
use the MapListener sub interfaces.
threads will evaluate segments of elements concurrently. And the amount of network traffic is reduced
drastically, since only filtered data is sent instead of all data.
Hazelcast provides two APIs for distributed queries.
1. Criteria API
2. Distributed SQL Query
Other Operators
In the Predicates class you can find a whole collection of useful operators.
notEqual: Checks if the result of an expression is not equal to a certain value.
instanceOf: Checks if the result of an expression has a certain type.
like: Checks if the result of an expression matches some string pattern. % (percentage sign) is a
placeholder for many characters, _ (underscore) is a placeholder for only one character.
greaterThan: Checks if the result of an expression is greater than a certain value.
greaterEqual: Checks if the result of an expression is greater than or equal to a certain value.
lessThan: Checks if the result of an expression is less than a certain value.
lessEqual: Checks if the result of an expression is less than or equal to a certain value.
between: Checks if the result of an expression is between two values (this is inclusive).
in: Checks if the result of an expression is an element of a certain collection.
isNot: Checks if the result of an expression is false.
regex: Checks if the result of an expression matches some regular expression.
If the predicates provided by Hazelcast are not enough, you can always write your own predicate by
implementing the Predicate interface:
isnt perfect. That is why a DSL (Distributed SQL Query) was added which is based on an SQL-like
language, and it uses the Criteria API underneath.
The getWithName function that we already implemented using the Criteria API can also be implemented
using the Distributed SQL Query:
age <= 30
name ="Joe"
age != 30
between
name
name
name
name
in
husband.mother.father.name=John
In this example, the name of the father of the mother of the husband should be John.
No arg methods: No arg methods can be called within a SQL predicate. In some cases, this is
useful if you dynamically need to calculate a value based on some properties. The syntax is the
same as for accessing a field.
5.14. Indexes
To speed up queries, just like in databases, the Hazelcast map supports indexes. Using an index
prevents iterating over all values. In database terms, this is called a full table scan, but it directly jumps
to the interesting ones. There are two types of indexes:
1. Ordered: for example, a numeric field where you want to do range searches like "bigger than".
2. Unordered: for example, a name field.
In the previous chapter, we talked about a Person that has a name, age, etc. To speed up searching on
these fields, we can place an unordered index on name and an ordered index on age:
<map name="persons">
<indexes>
<index ordered="false">name</index>
<index ordered="true">age</index>
</indexes>
</map>
The ordered attribute defaults to false.
To retrieve the index field of an object, first an accessor method will be tried. If that doesnt exist, a
direct field access is done. With the index accessor method, you are not limited to returning a field, you
can also create a synthetic accessor method where a value is calculated on the fly. The index field also
supports object traversal, so you could create an index on the street of the address of a person using
address.street. There is no limitation on the depth of the traversal. Hazelcast doesnt care about the
accessibility of the index field or accessor method, so you are not forced to make them public. An index
field or an object containing a field (for the x.y notation) is allowed to be null.
Hazelcast 3 has a big difference from Hazelcast 2: in Hazelcast 3, indexes can be created on the fly.
Management Center even has an option to create an index on an existing IMap.
The performance impact of using one or more indexes depends on several factors; among them are the
size of the map and the chance of finding the element with a full table scan. Other factors are adding
one or more indexes, making mutations to the map more expensive since the index needs to be
updated as well. So it could be that if you have more mutations than searches, that the performance
with an index is lower than without an index. It is recommended that you test in a production-like
environment, using a representative size/quality of the dataset, to see which configuration is best for
you. In the source code of the book, you have very rudimentary index benchmarks, one for updating
and one for searching.
In Hazelcast versions prior to 3.0, indexing for String fields was done only for the first 4 characters.
With Hazelcast version 3.0+, indexing is done on the entire String.
In the example, the indexes are placed as attributes of basic data types like int and String. But the IMap
allows indexes to be placed on an attribute of any type, as long as it implements Comparable. So you can
create indexes on custom data types.
5.15. Persistence
In the previous section, we talked about backups that protect against member failure: if one member
goes down, another member takes over. But it does not protect you against cluster failure: for example,
when a cluster is hosted in a single datacenter, and it goes down. Luckily, Hazelcast provides a solution
loading and storing data externally, such as in a database. This can be done using:
1. com.hazelcast.core.MapLoader: Useful for reading entries from an external datasource, but changes
dont need to be written back.
2. com.hazelcast.core.MapStore: Useful for reading and writing map entries from and to an external
datasource. The MapStore interface extends the MapLoader interface.
One instance per Map per Node will be created.
The following example shows an extremely basic HSQLDB implementation of the MapStore where we
load/store a simple Person object with a name field:
public PersonMapStore() {
try {
con = DriverManager.getConnection("jdbc:hsqldb:mydatabase", "SA", "");
con.createStatement().executeUpdate(
"create table if not exists person (id bigint, name varchar(45))");
} catch (SQLException e) {throw new RuntimeException(e);}
}
@Override
public synchronized void delete(Long key) {
try {
con.createStatement().executeUpdate(
format("delete from person where id = %s", key));
} catch (SQLException e) {throw new RuntimeException(e);}
}
@Override
public synchronized void store(Long key, Person value) {
try {
con.createStatement().executeUpdate(
format("insert into person values(%s,'%s')", key, value.name));
} catch (SQLException e) {throw new RuntimeException(e);}
}
@Override
public synchronized void storeAll(Map<Long, Person> map) {
for (Map.Entry<Long, Person> entry : map.entrySet())
store(entry.getKey(), entry.getValue());
}
@Override
public synchronized void deleteAll(Collection<Long> keys) {
for(Long key: keys) delete(key);
}
@Override
public synchronized Person load(Long key) {
try {
ResultSet resultSet = con.createStatement().executeQuery(
format("select name from person where id =%s", key));
try {
if (!resultSet.next()) return null;
String name = resultSet.getString(1);
return new Person(name);
} finally {resultSet.close();}
} catch (SQLException e) {throw new RuntimeException(e);}
}
@Override
public synchronized Map<Long, Person> loadAll(Collection<Long> keys) {
Map<Long, Person> result = new HashMap<Long, Person>();
for (Long key : keys) result.put(key, load(key));
return result;
}
Override
public Iterator<Long> loadAllKeys() {
return null;
}
}
The implementation is simple and certainly can be improved, such as transactions, prevention against
SQL injection, etc. Because the MapStore/MapLoader can be called by threads concurrently, this
implementation makes use of synchronization to deal with that correctly. Currently, it relies on a
course grained locked, but you could perhaps apply finer grained locking based on the key and a
striped lock.
To connect the PersonMapStore to the persons map, we can configure it using the map-store setting:
<map name="persons">
<map-store enabled="true">
<class-name>PersonMapStore</class-name>
</map-store>
</map>
In the following code fragment, you can see a member that writes a person to the map and then exits
the JVM. Then, you can see a member that loads the person and prints it.
and
the
MapLoader
called
only
when
one
of
the
members
calls
the
HazelcastInstance.getMap(name). If your application returns the map up front without needing the
content, you could wrap the map in a lazy proxy that calls the getMap method only when it is really
needed.
Prior to 3.5 the loadAllKeys method would execute on every member of the cluster. This had the
potential to overload the back-end stores that were being called. Since 3.5 loadAllKeys is now run just
once, the member to execute the method is selected by hashing the map name and deriving a partition
id. The member owning the partition id is the one that runs loadAllKeys.
Additionally the return type of loadAllKeys has changed, where it previously returned a Set<K> it now
returns an Iterator<K>. The iterator streams results back to the members that own the keys calling
loadAll in batches. The batch size is by default set to 1000, but can be changed using the property
hazelcast.map.load.chunk.size
You need to be aware that the map only knows about map entries that are in the memory; when a get
is done for an explicit key, then the map entry is loaded from the MapStore. This behavior is called "read
through". So if the loadAll returns a subset of the keys in the database, then the Map.size() will show
only the size of this subset, not the record count in the database. The same goes for queries; these will
only be executed on the entries in memory, not on the records in the database.
To make sure that you only keep hot entries in the memory, you can configure the time-to-live-
seconds property on the map. When a map entry isnt used and the time to live expires, it will
automatically be removed from the map without having to call MapStore.delete.
5.15.3. MapLoaderLifecycleSupport
In some cases, your MapLoader needs to be notified of lifecycle events. You can do this by having your
MapLoader implement the com.hazelcast.core.MapLoaderLifecycleSupport interface.
init: Useful if you want to initialize resources, such as opening database connections. One of the
parameters the init method receives is a Properties object. This is useful if you want to pass
properties from the outside to the MapLoader implementation. If you make use of the XML
configuration, in the map-store XML configuration, you can specify the properties that need to be
passed to the init method.
destroy: Useful if you need to cleanup resources, such as closing database connections.
5.16. MultiMap
In some cases you need to store multiple values for a single key. You could use a normal collection as
value and store the real values in this collection. This works fine if everything is done in the memory,
but in a distributed and concurrent environment it isnt that easy. One problem with this approach is
that the whole collection needs to be deserialized for an operation such as add. Imagine a collection of
100 elements; then 100 elements need to be deserialized when the value is read, and 101 items are
serialized when the value is written, for a total of 201 elements. This can cause a lot of overhead, CPU,
memory, network usage, etc. Another problem is that without additional concurrency control, such as
using a lock or a replace call, you could run into a lost update which can lead to issues like items not
being deleted or getting lost. To solve these problems, Hazelcast provides a MultiMap where multiple
values can be stored under a single key.
The MultiMap doesnt implement the java.util.Map interface since the signatures of the methods are
different. The MultiMap does have support for most of the IMap functionality (locking, listeners, etc.), but
it doesnt support indexing, predicates, and the MapLoader/MapStore.
To demonstrate the MultiMap, we are going to create two members. The PutMember will put data into the
MultiMap:
b -> [3]
a -> [2, 1]
As you can see, there is a single value for key b and 2 values for key a.
5.16.1. Configuration
The MultiMap is configured with the MultiMapConfig using the following configuration options.
valueCollectionType: The collection type of the value. There are 2 options: SET and LIST. With a set,
duplicate and null values are not allowed and ordering is irrelevant. With the list, duplicates and
null values are allowed and ordering is relevant. Defaults to SET.
listenerConfigs: The entry listeners for the MultiMap.
binary: If the value is stored in binary format (true) or in object format (false). Defaults to true.
backupCount: The number of synchronous backups. Defaults to 1.
asyncBackupCount: The number of asynchronous backups. Defaults to 0.
statisticsEnabled: If true, the statistics have been enabled.
The statistics can be accessed by calling the MultiMap.getLocalMultiMapStats() method.
and
vice
versa.
Also,
when
changes
are
made
on
these
collections,
an
UnsupportedOperationException is thrown.
Serialization: Although the IMap looks like an in-memory data structure, like a HashMap, there are
differences. For example, (de)serialization needs to take place for a lot of operations. Also
remoting could be involved. This means that the IMap will not have the same performance
characteristics as an in-memory map. To minimize serialization cost, make sure you correctly
configure the in-memory-format.
Size: Method is a distributed operation; a request is sent to each member to return the number of
map entries they contain. This means that abusing the size method could lead to performance
problems.
Memory Usage: A completely empty IMap instance consumes >200 KBs of memory in the cluster
with a default configured number of partitions. So having a lot of small maps could lead to
unexpected memory problems. If you double the number of partitions, the memory usage will
roughly double as well.
class EchoService{
private final ExecutorService =
Executors.newSingleThreadExecutor();
public void echoAsynchronously(final String msg){
executor.execute(new Runnable(){
public void run() {
System.out.println(msg);
}
});
}
}
So while a worker thread is processing the task, the thread that submitted the task is free to work
asynchronously. There is virtually no limit in what you can do in a task: you can perform complex
database operations, perform intensive CPU or I/O operations, render images, etc.
However, the problem in a distributed system is that the default implementation of the Executor,
which is the ThreadPoolExecutor, is designed to run within a single JVM. In a distributed system, you
want that a task submitted in one JVM can be processed in another JVM. Luckily, Hazelcast provides
the IExecutorService, which extends the java.util.concurrent.ExecutorService. It is designed to be used
in a distributed environment. The IExecutorService is new in Hazelcast 3.x.
Lets start with a simple example of IExecutorService, where a task is executed that does some waiting
and echoes a message.
the
differences
between
Hazelcast
2.x
and
Hazelcast
3.x
is
that
the
<executor-service name="exec">
<pool-size>1</pool-size>
</executor-service>
Another difference from Hazelcast 2.x is that the core-pool-size and keep-alive-seconds properties
have disappeared, so the pool will have a fixed size.
When the MasterMember is started, you will get output like this:
Producing
Producing
Producing
Producing
Producing
echo:1
....
echo
echo
echo
echo
echo
task:
task:
task:
task:
task:
1
2
3
4
5
The production of messages is 1 per second and the processing is 0.2 per second (the echo task sleeps 5
seconds). This means that we produce work 5 times faster than we are able to process it. Apart from
making the EchoTask faster, there are 2 dimensions for scaling:
1. Scale up
2. Scale out
Both are explained below. In practice, they are often combined.
6.1. Scaling Up
Scaling up, also called vertical scaling, is done by increasing the processing capacity on a single JVM.
Since each thread in the example can process 0.2 messages/second and we produce 1 message/second,
if the Executor has 5 threads it can process messages as fast as they are produced.
When you scale up, you need to look carefully at the JVM to see if it can handle the additional load. If
not, you may need to increase its resources (CPU, memory, disk, etc.). If you fail to do so, the
performance could degrade instead of improving.
Scaling up the ExecutorService in Hazelcast is simple, just increment the maximum pool size. Since we
know that 5 threads is going to give maximum performance, lets set them to 5.
<executor-service name="exec">
<pool-size>5</pool-size>
</executor-service>
Producing
Producing
Producing
Producing
Producing
echo:1
Producing
echo:2
Producing
echo:3
Producing
echo:4
...
echo
echo
echo
echo
echo
task:
task:
task:
task:
task:
1
2
3
4
5
echo task: 6
echo task: 7
echo task: 8
As you can see, the tasks are being processed as quickly as they are being produced.
import com.hazelcast.core.*;
public class SlaveMember {
public static void main(String[] args) {
Hazelcast.newHazelcastInstance();
}
}
We dont need to do anything else. This member will automatically participate in the executor that was
started in the master node and start processing tasks.
If one master and slave are started, you will see that the slave member is processing tasks as well:
echo:31
echo:33
echo:35
So in only a few lines of code, we are now able to scale out! If you want, you can start more slave
members, but with tasks being created at 1 task per second, maximum performance is reached with 4
slaves.
6.3. Routing
Until now, we didnt care which member did the actual processing of the task, as long as a member
picks it up. But in some cases you want to have that control. Luckily, the IExecutorService provides
different ways to route tasks.
1. Any member. This is the default configuration.
2. A specific member.
3. The member hosting a specific key.
4. All or subset of the members.
In the previous section, we already covered the first way: routing to any member. In the following
sections, well explain the last 3 routing strategies. This is where a big difference is visible between
Hazelcast 2.x and 3.x: while 2.x relied on the DistributedTask, 3.x relies on explicit routing methods on
the IExecutorService.
When we start a few slaves and a master, well get output like:
Members [2] {
Member [192.168.1.100]:5702 this
Member [192.168.1.100]:5703
}
...
echo/192.168.1.100:5702
As you can see, the EchoTasks are executed on the correct member.
key is local:true
key is local:true
...
The tasks are executed on the same member as where the data resides.
From Hazelcast 2.x, an alternative way of executing a request on a specific member has been to let the
task implement the HazelcastPartitionAware interface and use the execute or submit method on the
IExecutorService. The HazelcastPartitionAware exposes the getPartitionKey method that the executor
uses to figure out the key of the partition to route to. If a null value is returned, any partition will do.
6.3.4. Futures
The Executor interface only exposes a single void execute(Runnable) method that can be called to have
a Runnable asynchronously executed. But in some cases, you need to synchronize on results: for
example, when you use a Callable or you just want to wait till a task completes. You can do this by
using the java.util.concurrent.Future in combination with one of the submit methods of the
IExecutorService.
To demonstrate the Future, we will calculate a Fibonacci number by wrapping the calculation in a
callable and synchronizing on the result.
Result: 5
When you run this application with 500 as argument, it will probably take more than 10 seconds to
complete and therefore the future.get will timeout. When the timeout happens, a TimeoutException is
thrown. If it doesnt timeout on your machine, it could be that your machine is very quick and you
need to use a smaller timeout. Unlike Hazelcast 2.x, in Hazelcast 3.0 it isnt possible to cancel a future.
One possible solution is to let the task periodically check if a certain key in a distributed map exists. A
task can then be cancelled by writing some value for that key. You need to take care removing keys to
prevent this map from growing; you can do this by using the time to live setting.
Work-queue
has
no
high
availability:
Each
member
creates
one
or
more
local
ThreadPoolExecutors with ordinary work queues that do the real work. When a task is
submitted, it is put on the work queue of that ThreadPoolExecutor and is not backed up by
Hazelcast. If something happens with that member, all unprocessed work will be lost.
Work-queue is not partitioned: Each member specific executor will have its own private work
queue. Once an item is placed on that queue, it will not be taken by a different member. So it
could be that one member has a lot of unprocessed work, and another is idle.
Work-queue by default has unbound capacity: This can lead to OutOfMemoryErrors because the
number of queued tasks can grow without being limited. You can solve this by setting the <queuecapacity> property on the executor service. If a new task is submitted while the queue is full, the
call will not block, but it immediately throws a RejectedExecutionException that needs to be dealt
with. Perhaps in the future, blocking with configurable timeout will be made available.
No Load Balancing: This is currently available for tasks that can run on any member. In the
future, there will probably be a customizable load balancer interface where load balancing could
be done on the number of unprocessed tasks, CPU load, memory load, etc. If load balancing is
needed, you can create an IExecutorService proxy that wraps the one returned by Hazelcast.
Using
the
members
from
the
ClusterService
or
member
information
from
7.1. ITopic
Hazelcast provides a publish/subscribe mechanism: com.hazelcast.core.ITopic is a distributed solution
for publishing messages to multiple subscribers. Any number of members can publish messages to a
topic, and any number of members can receive messages for the topics they have subscribed to. The
message can be an ordinary POJO, although it must be able to serialize (see Serialization) since it needs
to go over the wire.
Well show you how the distributed topic works based on a simple example where a single topic is
shared between a publisher and a subscriber. The publisher publishes the current date on the topic.
<topic name="topic">
<message-listeners>
<message-listener>MessageListenerImpl</message-listener>
</message-listeners>
</topic>
Hazelcast uses reflection to create an instance of the MessageListenerImpl. For this to work, this class
needs to have a no-arg constructor. If you need more flexibility creating a MessageListener
implementation, you could have a look at the programmatic configuration where you can pass an
explicit instance instead of a class.
Topics use same event queue as other components of Hazelcast, which further means sharing
of resources. Having lot of events being created and transmitted inside the cluster may result
in unpredictable performance of Hazelcast Topics.
Also, there could be instances of unprocessed events due to slow listeners, and this may affect
other Topics as they share the same queue to publish and deliver messages.
There could also be scenarios where a message could be lost, such as:
to prevent the system from going OOM, the event queue is normally restricted with
capacity (default is 1,000,000). So, if the queue is full and a message arrives, the event gets
dropped.
if a member receives a message and the member crashes, the message is lost.
if a member sends a message on a topic but the message is still on transmission queue of
the connection and the member crashes, the message is lost even though the topic.publish
completed long time ago.
To address these very precise corner cases and add to the richness of ITopic, Hazelcast has introduced
a new structure ReliableTopic. It can be initialised as:
Each RingBuffer can have variable capacity, which allows you to restrict the number of messages
that can be published on a Topic.
For slow subscribers, ReliableTopic does not allow other components of the cluster to slow down.
In default situations, a slow listener will fall behind and may not receive the message it subscribed
for. Check out ReliableMessageListener to learn how to obtain more control over the functioning of
ReliableTopic with slow listeners.
While RingBuffer provides high performance reads, it also gives the reader the ability to reread a
message, multiple times in case of an error. Hazelcast also performs appropriate responsive
operations in various events such as capacity breach, etc. See TopicOverloadPolicy for more details.
long because the onMessage is called by a Thread of the event system, and hogging this thread could
lead to problems in the system.
In this case, there is only a single topic and we have a single executor. But you could decide to create
an executor per ITopic. If you dont care about ordering of the messages, you could use an ordinary
executor instead of a striped executor.
statics
in
the
map;
the
new
timestamp
can
be
determined
using
the
Cluster.getClusterTime() method. If there is no change, and the time period between the current
timestamp and the last timestamp exceeds a certain threshold, the topic and the entry in the
topic statistics map can be destroyed. This solution isnt perfect, since it could happen that a
message is sent to a topic that has been destroyed; the topic will be recreated, but the subscribers
are gone.
No durable subscriptions: If a subscriber goes offline, it will not receive messages that were sent
while it was offline.
No metadata: The message is an ordinary POJO and therefore it doesnt contain any metadata
(like a timestamp or an address) to reply to. Luckily, this can be solved by wrapping the message
<topic name="topic">
<statistics-enabled>true</statistics-enabled>
</topic>
The statistics, like total messages published/received, can only be accessed from cluster members
using topic.getLocalTopicStats. Topic statistics cant be retrieved by the client, because only a
member has knowledge about what happened to its topic. If you need to have global statistics,
you need to aggregate the statistics of all members.
You will see that the server prints Hello. If you look in the log for the full member, you will see that the
client never pops up as a member of the cluster.
In this example, we use programmatic configuration for the ClientConfig. You can also configure a
ClientConfig using the configuration file by:
using
properties
based
configuration
file
in
combination
with
the
with
the
com.hazelcast.client.config.ClientConfigBuilder.
using
an
XML
based
configuration
file
in
combination
com.hazelcast.client.config.XmlClientConfigBuilder.
The advantage of configuring the Hazelcast client using a configuration file is that you can easily pull
the client configuration out of the code, which makes the client more flexible. For example, you could
use a different configuration file for every environment you are working in (dev, staging, production).
In some cases, the static nature of the configuration files can be limiting if you need to have dynamic
information, such as the addresses. For that, you can first load the ClientConfig using a configuration
file, and then adjust the dynamic fields.
If you create a HazelcastInstance using the following code:
HazelcastInstance hz = HazelcastClient.newHazelcastClient()
Then Hazelcast will use the following sequence of steps to determine the client configuration file to
use.
1. Hazelcast checks if there is a system property hazelcast.client.config. If it exists, it is used. This
means that you can configure the configuration to use from the command line using
-Dhazelcast.client.config=/foo/bar/client.xml. You can also refer to a classpath resource using
-Dhazelcast.client.config=classpath:client.xml. This makes it possible to bundle multiple
configurations in your JAR and select one on startup.
2. Checks if there is a file called hazelcast-client.xml in the working directory.
3. Checks if there is a file called hazelcast-client.xml on the classpath.
4. Defaults to hazelcast-client-default.xml, which is provided by Hazelcast.
If you dont configure anything, the client will use the default configuration.
With off-heap capabilities, Hazelcast is now seen as a primary player in the vertical of Cache-AsA-Service use cases in a multi-tenant application. Normally in such use cases, many clients
communicate with a central repository of servers holding tons of data in memory and consume
cache as a service from this cluster. This sometimes causes complexities when keeping both
client and servers on the same version was mandatory: upgrades require downtimes and
additional maintenance cost. In some cases it is just not possible to upgrade all instances/groups
of application clients together.
From Hazelcast 3.5, a new Java native client library hazelcast-client-new-xxx.jar will be
available in the release package. This library uses Hazelcasts new client protocol which provides
client and server version independent compliance. With the new protocol, Hazelcast will allow
any client to work with any version of server with 3.5 and above. It will also allow backward
compatibility, any client of version 3.x (where x > 5) will be able to connect with servers on lower
version up to 3.5.
With the new client protocol, Hazelcast provides greater flexibility to maintain a cluster of client
and server nodes of different versions and allows maximum robustness.
recreated. If the value is set too high, it can lead to dead members being detected very late.
connectionAttemptLimit: Maximum number of times to try using the addresses to connect to the
cluster. Defaults to 2. When a client starts or a client loses the connection with the cluster, it will try
to make a connection with one of the cluster member addresses. In some cases, a client cannot
connect to these addresses; for example, the cluster is not yet up or it is not reachable. Instead of
giving up, one can increase the attempt limit to create a connection. Also have a look at the
connectionAttemptPeriod.
connectionAttemptPeriod: Period in milliseconds between attempts to find a member in the cluster.
Defaults to 3000 milliseconds.
listeners: Enables listening to the cluster state. Currently, only the LifecycleListener is supported.
loadBalancer: See LoadBalancing for more information. Defaults to RoundRobinLB.
smart: If true, the client will route the key based operations to the owner of the key at the best
effort. Note that it uses a cached version of PartitionService.getPartitions() and it does not
guarantee that the operation will always be executed on the owner. The cached table is updated
every second. Defaults to true.
redoOperation: If true, the client will redo the operations that were executing on the server when
the client lost the connection. This can be because of the network, or simply because the member
died. However, it is not clear whether the application is performed or not. For idempotent
operations this is harmless, but for non-idempotent operations, retrying can cause undesirable
effects. Note that the redo can perform on any member. If false, the operation will throw the
RuntimeException that is wrapping IOException. Defaults to false.
group: See Group Configuration.
socketOptions: Configures the network socket options with the methods setKeepAlive(x),
setTcpNoDelay(x), setReuseAddress(x), setLingerSeconds(x), and setBufferSize(x).
serializationConfig: Configures how to serialize and deserialize on the client side. For all classes
that are deserialized to the client the same serialization needs to be configured as done for the
cluster. For more information see Serialization.
socketInterceptor: Allows you to intercept socket connections before a node joins to cluster or a
client connects to a node. This provides the ability to add custom hooks to join and perform
connection procedures.
classLoader: In Java, you can configure a custom classLoader. It will be used by the serialization
service and to load any class configured in configuration, such as event listeners or ProxyFactories.
credentials: Can be used to do authentication and authorization. This functionality is only
available in the Enterprise version of Hazelcast.
8.3. LoadBalancing
When a client connects to the cluster, it will have access to the full list of members and it will be kept in
sync, even if the ClientConfig only has a subset of members. If an operation needs to be sent to a
specific member, it will be sent directly to that member. If an operation can be executed on any
member, Hazelcast does automatic load balancing over all members in the cluster.
One of the new features that came with Hazelcast 3.0 is that the routing mechanism is pulled into an
interface:
8.4. Failover
In a production environment, you want the client to support failover to increase high availability. This
is realized in two parts.
The first part is configuring multiple member addresses in the ClientConfig. As long as one of these
members is online, the client will be able to connect to the cluster and will know about all members in
the cluster.
The second part is the responsibility of the LoadBalancer implementation. It can register itself as a
MembershipListener and receives a list of all members in the cluster, and it will be notified if members
are added or removed. The LoadBalancer can use this update list of member addresses for routing.
8.7. SSL
In Hazelcast 3, you can encrypt communication between client and cluster using SSL. This means that
the whole network traffic, which includes normal operations like a map.put and includes passwords in
credentials and GroupConfig, cannot be read and potentially modified.
keytool -genkey -alias hazelcast -keyalg RSA -keypass password -keystore hazelcast.ks
-storepass password
keytool -export -alias hazelcast -file hazelcast.cer -keystore hazelcast.ks -storepass
password keytool
-import -v -trustcacerts -alias hazelcast -keypass password -file hazelcast.cer -keystore
hazelcast.ts
-storepass password
Example SSL configuration of the server:
Shutdown: If you dont need a client anymore, it is very important to shut it down using the
shutdown method or using the LifeCycleService.
client.getLifecycleService().shutdown();
The reason why the client shutdown is important, especially for short lived clients, is that the
shutdown releases resources. It will shutdown the client thread pool and the connection pool.
When a connection is closed, the client/member socket is closed and the ports are released,
making them available for new connections. Network traffic is also reduced since the heartbeat
does not need to be sent anymore. And the client resources running on the cluster are released,
such as the EndPoint or distributed Locks that have been acquired by the client. If the client is
not shutdown, and resources like the Lock have not been released, every thread that wants to
acquire the lock is going to deadlock.
SPI: The Hazelcast client can also call SPI operations, see SPI. But you need to make sure that the
client has access to the appropriate classes and interfaces.
2 way clients: There are cases where you have a distributed system split in different clusters, but
there is a need to communicate between the clusters. Instead of creating one big Hazelcast
cluster, it could be split up in different groups. To be able to have each group communicate with
the other groups, create multiple clients. If you have two groups A and B, then A should have a
client to B and B should have a client to A.
HazelcastSerializationException: If you run into this exception with the message "There is no
suitable serializer for class YourObject", then you have probably forgotten to configure the
SerializationConfig for the client. See Serialization. In many cases, you want to copy/paste the
whole serialization configuration of the server to make sure that the client and server are able to
serialize/deserialize the same classes.
None smart clients and load balancing: If a client is not smart, it will randomize the members list
and try to connect to one of these members until it succeeds. So if you have a 16 node cluster,
and 2 members are configured in the client, all load will go through these members. The
consequence is that load isnt equally spread over the members. Try to add as many members in
the client configuration as possible to balance the load better.
Chapter 9. Serialization
So far, the examples in this book have relied on standard Java serialization by letting the objects we
store in Hazelcast implement the java.io.Serializable interface. But Hazelcast has a very advanced
serialization system that supports native Java serialization, such as Serializable and Externalizable.
This is useful if you dont own the class and therefore cant change its serialization mechanism. But it
also supports custom serialization mechanisms like DataSerializable, Portable, ByteArraySerializer
and ByteStreamSerializer.
In Hazelcast, when an object needs to be serialized (for example, because the object is placed in a
Hazelcast data structure like a map or queue), Hazelcast first checks if the object is an instance of
DataSerializable or Portable. If that fails, Hazelcast checks if the object is a well known type, such as
String, Long, Integer, byte[], ByteBuffer, or Date, since serialization for these types can be optimized.
Then, Hazelcast checks for user specified types, such as ByteArraySerializer and ByteStreamSerializer.
If that fails, Hazelcast will fall back on Java serialization (including the Externalizable). If this also fails,
the serialization fails because the class cannot be serialized. This sequence of steps is useful to
determine which serialization mechanism is going to be used by Hazelcast if a class implements
multiple interfaces, such as Serializable and Portable.
Whatever serialization technology is used, if a class definition is needed, Hazelcast will not
automatically download it. So you need to make sure that your application has all the classes it needs
on the classpath.
9.1. Serializable
The native Java serialization is the easiest serialization mechanism to implement, since a class often
only needs to implement the java.io.Serializable interface.
When you use serialization, because you dont have exact control on how an Object is (de)serialized,
you dont control the actual byte content. In most cases, this wont be an issue, but if you are using a
method that relies on the byte-content comparisons, and the byte-content of equal objects is different,
then
you
get
unexpected
behavior.
An
example
of
such
method
is
the
Imap.replace(key,expected,update), and an example of a serialized data structure with unreliable bytecontent is a HashMap. So if your expected class directly or indirectly relies on a HashMap, the replace
method could fail to replace keys.
9.2. Externalizable
Another serialization technique supported by Hazelcast is the java.io.Externalizable. It provides more
control on how the fields are serialized/deserialized and it can also help to improve performance
compared to standard Java serialization. Here is an example of the Externalizable in action.
9.3. DataSerializable
Although Java serialization is very easy to use, it comes at a price.
Java serialization has lack of control on how the fields are serialized/deserialized.
It also has suboptimal performance due to streaming class descriptors, versions, keeping track of
seen objects to deal with cycles, etc. This causes additional CPU load and suboptimal size of
serialized data.
That is why in Hazelcast 1, the DataSerializable serialization mechanism was introduced.
To see the DataSerializable in action, lets implement on the Person class:
Person(name=Peter)
9.3.1. IdentifiedDataSerializable
One of the problems with DataSerializable is that it uses reflection to create an instance of the class.
One of the new features of Hazelcast 3 is the IdentifiedDataSerializable. It relies on a factory to create
the instance and therefore is faster when deserializing, since deserialization relies on creating new
instances.
The first step is to modify the Person class to implement the IdentifiedDataSerializable interface.
PersonDataSerializableFactory.
So
you
can
have
IdentifiedDataSerializable
implementations that return the same ID, as long as the getFactoryId is different. You could move the
IDs to the DataSerializableFactory implementation to have a clear overview.
The next part is to create a PersonDataSerializableFactory which is responsible for creating an instance
of the Person class.
<hazelcast>
<serialization>
<data-serializable-factories>
<data-serializable-factory
factory-id="1">PersonDataSerializableFactory</data-serializable-factory>
</data-serializable-factories>
</serialization>
</hazelcast>
If you look closely, you see that the PersonDataSerializableFactory.FACTORY_ID has the same value as
the factory-id field in the XML. This is very important since Hazelcast relies on these values to find the
right DataSerializableFactory when deserializing.
To see the IdentifiedDataSerializable in action, have a look at the following example.
Person(name=Peter)
9.4. Portable
With the introduction of Hazelcast 3.0, a new serialization mechanism was added: the Portable. The
cool thing about the Portable is that object creation is pulled into user space, so you control the
initialization of the Portable instances and you are not forced to use a no-argument constructor. For
example, you could inject dependencies or you could even decide to move the construction of the
Portable from a prototype bean in a Spring container.
To demonstrate how the Portable mechanism works, lets create a Portable version of the Person class.
The last two interesting methods are getClassId, which returns the identifier of that class, and
getFactoryId, which must return the ID of the PortableFactory that will take care of serializing and
deserializing.
The next step is the PortableFactory which is responsible for creating a new Portable instance based on
the class ID. In our case, the implementation is very simple since we only have a single Portable class.
import com.hazelcast.nio.serialization.*;
public class PortableFactoryImpl implements PortableFactory {
public final static int PERSON_CLASS_ID = 1;
public final static int FACTORY_ID = 1;
@Override
public Portable create(int classId) {
switch (classId) {
case PERSON_CLASS_ID:
return new Person();
}
return null;
}
}
In practice, the switch case probably will be a lot bigger. If an unmatched classId is encountered, null
should be returned, which will lead to a HazelcastSerializationException. A class ID needs to be unique
within the corresponding PortableFactory and needs to be bigger than 0. You can declare the class ID in
the class to serialize, but you could add it to the PortableFactory to have a good overview of which IDs
are there.
A factory ID needs to be unique and larger than 0. You probably will have more than one
PortableFactory. To make sure that every factory gets a unique factory ID, you could make a single
class/interface where all PortableFactory IDs in your system are declared, as shown below.
<serialization>
<portable-factories>
<portable-factory factory-id="1">PortableFactoryImpl</portable-factory>
</portable-factories>
</serialization>
Hazelcast can have multiple portable factories. You need to make sure that the factory-id in the XML is
the same as in the code.
Of course we also want to see it in action:
Serialize
Serialize
Deserialize
Person(name=Peter)
The Person is serialized when it is stored in the map and it is deserialized when it is read. Serialize is
called twice because for every Portable class, the first time it is (de)serialized, Hazelcast generates a
new class that supports the serialization/deserialization process. For this generation process, another
serialization is executed to figure out the metadata (the fields and their types).
The names of the fields are case-sensitive and need to be valid java identifiers. Therefore, they should
not contain characters such as . or -.
the payload of every serialized object. So the amount of data transferred with a Portable is a lot more
than with a DataSerializable. If you want to use the fastest serialization mechanism, it is best to have a
look at the IdentifiedDataSerializable, since no field metadata is send over the line.
potentially
using
XML,
to
maintain
platform
compatibility.
The
methods
readUTF/writeUTF can perfectly deal with null Strings, so passing null object references is no problem.
is set after deserialization, you need to store the IDs first, and then you could do the actual retrieval of
the distributed objects in the setHazelcastInstance method.
9.4.5. Cycles
One thing to look out for, which also goes for DataSerializable, are cycles between objects: they can
lead to a stack overflow. Standard Java serialization protects against this, but since manual traversal is
done in Portable objects, there is no protection out of the box. If this is an issue, you could store a map
in a ThreadLocal that can be used to detect cycles and a special placeholder value could be serialized to
end the cycle.
9.4.6. Subtyping
Subtyping with the Portable functionality is easy: let every subclass have its own unique type ID, and
then add these IDs to the switch/case in the PortableFactory so that the correct class can be
instantiated.
9.4.7. Versioning
In practice, multiple versions of the same class could be serialized and deserialized, such as a Hazelcast
client with an older Person class compared to the cluster. Luckily, the Portable functionality supports
versioning. In the configuration, you can explicitly pass a version using the <portable-version> tag
(defaults to 0).
<serialization>
<portable-version>1</portable-version>
<portable-factories>
<portable-factory factory-id="1">PortableFactoryImpl</portable-factory>
</portable-factories>
</serialization>
When a Portable instance is deserialized, apart from the serialized fields of that Portable, metadata like
the class id and the version are also stored. That is why it is important that every time you make a
change in the serialized fields of a class, that the version is also changed. In most cases, incrementing
the version is the simplest approach.
Adding fields to a Portable is simple. However, you probably need to work with default values if an old
Portable is deserialized.
Removing fields can lead to problems if a new version of that Portable (with the removed field) is
deserialized on a client that depends on that field.
Renaming fields is simpler because the Portable mechanism does not rely on reflection, so there is no
automatic mapping of fields on the class and fields in the serialized content.
An issue to watch out for is changing the field type, although Hazelcast can do some basic type
upgrading (for example, int to long or float to double).
Renaming the Portable is simple since the name of the Portable is not stored as metadata, but the class
ID (which is a number) is stored.
Luckily, Hazelcast provides access to the metadata of the object to be deserialized through the
PortableReader; the version, available fields, the type of the fields, etc., can be retrieved. So you have
full control on how the deserialization should take place.
HazelcastInstanceAware: The PortableFactory can be combined with the HazelcastInstanceAware. Thus,
you can control if dependencies are going to be passed to the Portable implementation.
9.5. StreamSerializer
One of the additions to Hazelcast 3 is to use a stream for serializing and deserializing data by
implementing the StreamSerializer. The StreamSerializer is practical if you want to create your own
implementations, and you can also use it to adapt an external serialization library, such as JSON,
protobuf, Kryo, etc.
Lets start with a very simple object we will serialize using a StreamSerializer.
<serialization>
<serializers>
<serializer
type-class="Person">PersonStreamSerializer</serializer>
</serializers>
</serialization>
In this case, we have registered the serializer PersonStreamSerializer for the Person class. When
Hazelcast is going to serialize an object, it looks up the serializer registered for the class for that object.
Hazelcast is quite flexible; if it fails to find a serializer for a particular class, it first tries to match based
on superclasses and then on interfaces. You could create a single StreamSerializer that can deal with a
class hierarchy if that StreamSerializer is registered for the root class of that class hierarchy. If you use
this approach, then you need to write sufficient data to the stream so that on deserialization, you can
figure out exactly which class needs to be instantiated.
It is not possible to create StreamSerializers for well known types like Long, String, primitive arrays,
etc., since Hazelcast already registers them.
Here is the serializer in action.
Person{name='peter'}
@Override
public void destroy() {
}
}
When the writeObject is called, Hazelcast will look up a serializer for the particular type. Hazelcast has
serializers available for the wrapper types like Long, Boolean, etc. Luckily, the writeObject (and
readObject) are perfectly able to deal with null.
To complete the example, the CarStreamSerializer also needs to be registered.
<serialization>
<serializers>
<serializer
type-class="Person">PersonStreamSerializer</serializer>
<serializer
type-class="Car">CarStreamSerializer</serializer>
</serializers>
</serialization>
If you run the following example:
Car{color='red', owner=Person{name='peter'}}
Traversing object graphs for serialization and reconstructing object graphs on deserialization is quite
simple. One thing you need to watch out for is cycles, see Cycles.
9.5.2. Collections
If the field of an object needs to be serialized with the stream serializer, then currently there is no
other solution except to write a custom serializer for that field. Support for collection serializers
probably will be added in the near future, but for the time being you might have a look at the following
two implementations. First, the serializer for the LinkedList.
<hazelcast>
<serialization>
<serializers>
<serializer type-class="Person">PersonKryoSerializer</serializer>
</serializers>
</serialization>
</hazelcast>
When we run the following example code:
Person(name=Peter)
In the previous example, we showed how Kryo can be implemented as a StreamSerializer. The cool
thing is that you can just plug in a serializer for a particular class; no matter if that class already
implements a different serialization strategy such as Serializable. If you dont have the chance to
implement Kryo as StreamSerializer, then you can also directly implement the serialization on the
class. You can do this by using the DataSerializable and (de)serializing each field using Kryo. This
approach is especially useful if you are still working on Hazelcast 2.x. Kryo is not the only serializable
library, you also might want to have a look at Jackson Smile, Protobuf, etc.
9.6. ByteArraySerializer
An alternative to the StreamSerializer is the ByteArraySerializer. With the ByteArraySerializer, the
raw bytearray internally used by Hazelcast is exposed. This is practical if you are working with a
serialization library that works with bytearrays instead of streams.
The following code example show the ByteArraySerializer in action.
<serialization>
<serializers>
<serializer
type-class="Person">PersonByteArraySerializer</serializer>
</serializers>
</serialization>
<serialization>
<serializers>
<global-serializer>PersonStreamSerializer
</global-serializer>
</serializers>
</serialization>
There can only be a single global serializer. For this global serializer, the StreamSerializer.getTypeId
method does not need to return a relevant value.
The global serializer can also be a ByteArraySerializer.
9.8. HazelcastInstanceAware
In some cases, when an object is deserialized, it needs access to the HazelcastInstance so that
distributed objects can be accessed. You can do this by implementing HazelcastInstanceAware, as in the
following example.
serialized.
Injecting a HazelcastInstance into a domain object (an Entity) like Person isnt going to win you a
beauty contest. But it is a technique you can use in combination with Runnable/Callable
implementations that are executed by an IExecutorService that sends them to another machine. After
deserialization of such a task, the implementation of the run/call method often needs to access the
HazelcastInstance.
A best practice for implementing the setHazelcastInstance method is only to set the HazelcastInstance
field and not execute operations on the HazelcastInstance. The reason behind this is that for some
HazelcastInstanceAware implementations, the HazelcastInstance isnt fully up and running when it is
injected.
You need to be careful with using the HazelcastInstanceAware on anything other than the root object
that is serialized. Hazelcast sometimes optimizes local calls by skipping serialization. Some
serialization technologies, like Java serialization, dont allow for applying additional logic when an
object graph is deserialized. In these cases, only the root of the graph is checked if it implements
HazelcastInstanceAware, but the graph isnt traversed.
9.8.1. UserContext
Obtaining dependencies other than a HazelcastInstance was more complicated in Hazelcast 2.x. Often
the only way was to rely on some form of static field. Luckily, Hazelcast 3 provides a new solution
using the user context: a (Concurrent)Map that can be accessed from the HazelcastInstance using the
getUserContext() method. In the user context, arbitrary dependencies can be placed using some key as
String.
Lets start with an EchoService dependency that we want to make available in an EchoTask which will be
executed using a Hazelcast distributed executor.
hello
It is possible to configure user-context on the Config, and you can also directly configure the usercontext of the HazelcastInstance. This is practical if you need to add dependencies on the fly. Do not
forget to clean up what you put in the user-context, else you might run into resource problems like an
OutOfMemoryError.
Changes made in the user-context are local to a member only. Other members in the cluster are not
going to observe changes in the user-context of one member. If you need to have that EchoService
available on each member, you need to add it to the user-context on each member.
It is important to know that when a HazelcastInstance is created using a Config instance, a new usercontext ConcurrentMap is created and the content of the user-context of the Config is copied. Therefore,
changes made to the user-context of the HazelcastInstance will not reflect on other HazelcastInstance
created using the same Config instance.
9.9. ManagedContext
In some cases, when a serialized object is deserialized, not all of its fields can be deserialized because
they are transient. These fields could be important data structures like executors, database
connections, etc. Luckily, Hazelcast provides a mechanism that is called when an object is deserialized
and gives you the ability to fix the object by setting missing fields and call methods, wrapping it inside
a proxy, etc., so it can be used. This mechanism is called the ManagedContext and you can configure it on
the SerializationConfig.
In the following example, we have a DummyObject with a serializable field named ser and a transient
field named trans.
DummyObject{ser='someValue', trans=Thread[Thread-2,5,main]}
DummyObject{ser='someValue', trans=Thread[Thread-3,5,main]}
The transient field has been restored to a new thread.
Hazelcast currently provides one ManagedContext implementation: the SpringManagedContext for
integration with Spring. If you are integrating with different products, such as Guice, you could
provide your own ManagedContext implementation.
If you need to have dependencies in your ManagedContext, you can let it implement the
HazelcastInstanceAware interface. You can retrieve custom dependencies using theUserContext.
You need to be careful using the ManagedContext on anything other than the root object that is
serialized. Hazelcast sometimes optimizes local calls by skipping serialization. Also, some serialization
technologies, like Java serialization, dont allow for applying additional logic when an object graph is
deserialized. In these cases, only the root of the object graph can be offered to the ManagedContext, but
the graph isnt traversed.
Nested operations: DataSerializable and Portable instances are allowed to call operations on
Hazelcast that lead to new serialize/deserialize operations. Unlike Hazelcast 2.x, it does not lead
to StackOverflowErrors.
Thread-safety: The serialization infrastructure, such as classes implementing DataSerializable,
Portable and support structures like TypeSerializer or PortableFactory, need to be threadsafe
since the same instances will be accessed by concurrent threads.
Encryption for in memory storage: In some cases, having raw data in memory is a potential
security risk. This problem can be solved by modifying the serialization behavior of the class so
that it encrypts the data on writing and decrypts on reading. In some cases, such as storing a
String in a map, the instance needs to be wrapped in a different type (for example,
EncryptedPortableString) to override the serialization mechanism.
Compression: The SerializationConfig has a enableCompression property which enables
compression for java.io.Serializable and java.io.Externalizable objects. If your classes make
use of a different serialization implementation, there is no out of the box support of
compression.
Performance: Serialization and deserialization will have an impact on performance, no matter
how fast the serialization solution is. That is why you need to be careful with operations on
Hazelcast data structures. For example, iterating over a normal HashMap is very fast, since no
serialization is needed, but iterating over a Hazelcast distributed map is a lot slower. This is
because potential remoting is involved, and also because data needs to be deserialized. Some
users have burned themselves on this issue because it isnt always immediately obvious from the
code.
Performance comparisons: For an overview of performance comparisons between the different
serialization
solutions,
you
could
have
look
at
the
following
blogpost:
http://tinyurl.com/qx5rpc2
Mixing serializers: If an object graph is serialized, different parts of the graph could be serialized
using different serialization technologies. It could be that some parts are serialized with
Portable, other parts with StreamSerializers and Serializers. Normally, this wont be an issue, but
if you need to exchange these classes with the outside world, it is best to have everything
serialized using Portable.
In Memory: Currently, all serialization is done in memory. If you are dealing with large object
graphs or large quantities of data, you need to keep this in mind. There is a feature request that
makes it possible to use streams between members and members/clients and to overcome this
memory limitation. Hopefully, this will be implemented in the near future.
Factory IDs: Different serialization technologies, such as Portable vs. IdentifiedDataSerializable,
dont need to be unique.
<map name="employees">
....
</map>
A TransactionalMap will have all the configuration options you have on a normal IMap. The same goes
for the TransactionalMultiMap, which is backed up by a MultiMap.
Because the TransactionalMap is build on top of the IMap, the TransactionalMap can be loaded as an IMap.
10.2. TransactionOptions
In some cases, the default behavior of the Transaction does not work and needs to be fine tuned. With
the Hazelcast transaction API, you can do this by using the TransactionOptions object and passing it to
the HazelcastInstance.newTransactionContext(TransactionOptions) method.
Currently, Hazelcast provides the following configuration options.
1. timeoutMillis: Time in milliseconds a transaction will hold a lock. Defaults to 2 minutes. In most
cases, this timeout is enough since the transaction should be executed quickly.
2. TransactionType: Either LOCAL or TWO_PHASE. See TransactionType.
3. durability: Number of backups for the transaction log, defaults to 1. See Partial Commit Failure for
more infomation.
The following fragment makes use of the TransactionOptions to configure a TransactionContext which is
TWO_PHASE, has a timeout of 1 minute, and a durability of 1.
10.2.1. TransactionType
With the TransactionType, you can influence how much guarantee you get when a member crashes
when a transaction is committing. Hazelcast provides two TransactionTypes. Their names are a bit
confusing.
1. LOCAL: Unlike the name suggests, LOCAL is a two phase commit. First, all cohorts are asked to prepare
if everyone agrees. Then, all cohorts are asked to commit. The problem happens if during the
commit phase one or more members crash; the system could be left in an inconsistent state
because some of the members might have committed and others might not.
2. TWO_PHASE: The two phase commit is more than the classic two phase commit (if you want a regular
two phase commit, use LOCAL). Before it commits, it copies the commit log to other members, so in
case of member failure, another member can complete the commit.
So which one should you use? It depends. LOCAL will perform better but TWO_PHASE will provide better
consistency in case of failure.
10.3. TransactionalTask
In the previous example, we manually manage the transaction; we manually begin one and manually
commit it when the operation is finished, and we manually rollback the transaction on failure. This
can cause a lot of code noise due to the repetitive boilerplate code. Luckily, this can be simplified by
using the TransactionalTask and the HazelcastInstance.executeTransaction(TransactionalTask) method.
This method automatically begins a transaction when the task starts and automatically commits it on
success or performs a rollback when a Throwable is thrown.
The previous example could be rewritten to use the TransactionalTask like this.
by
calling
the
transaction-2 modifies key foo and increments the value from 0 to 1 and commits before transaction-1
commits, and then transaction-1 reads the value of key foo again, it will see 1 as value. This is called
non-repeatable read.
If you want not to face any non-repeatable reads, which means the isolation level is
REPEATABLE_READ, all reads should be done with using the method getForUpdate() of
TransactionalMap. This method uses locks to prevent non-repeatable reads.
10.6. Locking
It is important to understand how the locking within Hazelcast transactions work. For example, if a
map.put is done, the transaction will automatically lock the entry for that key for the remaining
duration of the transaction. If another transaction wants to do an update on the same key, it also wants
to acquire the lock and will wait till the lock is released or the transaction runs into a timeout (see
TransactionOptions.timeoutMillis).
Reads on a map entry will not acquire the lock but reads will be blocked if another transaction has
acquired that lock; therefore, one transaction is not able to read map entries locked by another
transaction. Reads dont block writes but writes can block reads. If you want to acquire the lock when
reading, check the TransactionalMap.getForUpdate method. This provides the same locking semantics as
a map.put and can be compared with the select for update SQL statement.
Hazelcast doesnt have fine grained locks like a readwritelock; the lock acquired is exclusive. If a lock
cant be acquired within a transaction, the operation will timeout after 30 seconds and throws an
OperationTimeoutException. This provides protection against deadlocks. If a lock was acquired
successfully, but its lock expires and therefore will be released, the transaction will happily continue
executing operations. Only when the transaction is preparing for commit, this released lock is detected
and a TransactionException is thrown. When a transaction aborts or commits, all locks are
automatically released. Also, when a transaction expires, its locks will automatically be released.
If you are automatically retrying a transaction that throws an OperationTimeoutException, and you do
not control the number of retries, it is possible that the system will run into a livelock. Livelocks are
even harder to deal with than deadlocks because the system appears to do something since the threads
are busy, but there is either not much or no progress due to transaction rollbacks. Therefore, it is best
to limit the number of retries and perhaps throw some kind of Exception to indicate failure.
10.8. XA Transactions
It is likely that an application system needs to manage multiple resources in the same transaction. As a
standard, XA describes the interface between the global transaction manager and the local resource
manager. XA allows multiple resources (such as databases, application servers, message queues,
transactional caches, etc.) to be accessed within the same transaction, thereby preserving the ACID
properties across applications. XA uses a two-phase commit to ensure that all resources either commit
or rollback any particular transaction consistently.
By implementing the XAResource interface, Hazelcast provides XA transactions and it is fully XAcompliant. You can obtain the HazelcastXAResource instance via HazelcastInstance. Below is example
code that uses Atomikos for transaction management:
<%@page
<%@page
<%@page
<%@page
<%@page
<%@page
<%@page
import="javax.resource.ResourceException" %>
import="javax.transaction.*" %>
import="javax.naming.*" %>
import="javax.resource.cci.*" %>
import="java.util.*" %>
import="com.hazelcast.core.*" %>
import="com.hazelcast.jca.*" %>
<%
UserTransaction txn = null;
HazelcastConnection conn = null;
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
try {
Context context = new InitialContext();
txn = (UserTransaction) context.lookup( "java:comp/UserTransaction" );
txn.begin();
HazelcastConnectionFactory cf = (HazelcastConnectionFactory)
context.lookup ( "java:comp/env/HazelcastCF" );
conn = cf.getConnection();
TransactionalMap<String, String> txMap = conn.getTransactionalMap( "default" );
txMap.put( "key", "value" );
txn.commit();
} catch ( Throwable e ) {
if ( txn != null ) {
try {
txn.rollback();
} catch ( Exception ix ) {
ix.printStackTrace();
};
}
e.printStackTrace();
} finally {
if ( conn != null ) {
try {
conn.close();
} catch (Exception ignored) {};
}
}
%>
10.10. Performance
Although transactions may be easy to use, their usage can influence the application performance
drastically due to locking and dealing with partial failed commits. Try to keep transactions as short as
possible so that locks are held for the least amount of time and the least amount of data is locked. Also
try to co-locate data in the same partition if possible.
No readonly support: Hazelcast transactions cant be configured as readonly. Perhaps this will be
added in the future.
No support for transaction propagation: It isnt possible to create nested transactions without
using XA transactions. If you do, an IllegalStateException will be thrown.
Hazelcast client: Transactions can also be used from the Hazelcast client.
Queue operations: For queue operations (offer, poll), offered and/or polled objects are copied to
the owner member in order to safely commit/rollback. Moreover; if an item is polled in a
transaction, then it is really polled and other poll operations returns different items or null if the
queue is empty during the transaction. If the transaction rollbacks, then the item is offered to the
queue again. The same applies for remove/add operations of TransactionalSet and
TransactionalList.
ITopic: There is no transactional ITopic. Perhaps this will be implemented in the future.
No thread locals: The Transaction API doesnt rely on thread local storage. If you need to offload
some work to a different thread, pass the TransactionContext to the other thread. The
transactional data structures can be passed as well, but the other thread could also retrieve them
again since a transactional data structure can be retrieved multiple times from the
TransactionContext instance.
MapStore and QueueStore: MapStore and QueueStore does not participate in transactions. Hazelcast
will suppress exceptions thrown by store in a transaction.
<hazelcast>
...
<network>
...
</network>
...
</hazelcast>
For brevity reasons, this example leaves out the enclosing Hazelcast tags. You can find the complete
sources for this book on the Hazelcast website. For a Hazelcast cluster to function correctly, all
members must be able to contact every other member. Hazelcast doesnt support connecting over a
member that is able to connect to another member.
11.2. Port
One of the most basic configuration settings is the port Hazelcast uses for communication between the
members. You can set this with the port property in the network configuration. It defaults to 5701.
<network>
<port>5701</port>
</network>
If you start the member, you will get output like the following.
<network>
<port port-count="200">5701</port>
</network>
In most cases, you wont need to specify the port-count attribute. But it can be very practical in those
rare cases where you need to.
If you only want to make use of a single explicit port, you can disable automatic port increment using
the auto-increment attribute (which defaults to true) as shown below.
<network>
<port auto-increment="false">5701</port>
</network>
The port-count property will be ignored when auto-increment is false.
If you look at the end of the logging, youll see the following warning:
You will get this warning no matter how many members you start. The cause is that if you use the XML
configuration, by default no join mechanism is selected and therefore the members cant join to form a
cluster. To specify a join mechanism, see Join Mechanism.
<network>
<outbound-ports>
<!-- ports between 33000 and 35000 -->
<ports>33000-35000</ports>
<!-- comma separated ports -->
<ports>37000,37001,37002,37003</ports>
<ports>38000,38500-38600</ports>
</outbound-ports>
</network>
And this is the programmatic version:
`TIME_WAIT state is ignored and you can bind the member to the same port again.
The following is how to configure it with XML.
<network>
<reuse-address>true</reuse-address>
</network>
Or, here is how to configure it in a programmatic way.
11.5.1. Multicast
With multicast discovery, a member will send a message to all members that listen to a specific
multicast group. It is the easiest mechanism to use, but it is not always available. Here is an example of
a very minimalistic multicast configuration:
<network>
<join>
<multicast enabled="true"/>
</join>
</network>
If you start one member, you will see output like this:
Members [2] {
Member [192.168.1.104]:5701 this
Member [192.168.1.104]:5702
}
The first member can see the second member. And if we look at the end of logging for the second
member, well find something similar:
Members [2] {
Member [192.168.1.104]:5701
Member [192.168.1.104]:5702 this
}
We now have a two-member Hazelcast cluster running on a single machine. It becomes more
interesting if you start multiple members on different machines.
You can tune the multicast configuration using the following elements.
1. multicast-group: With multicast, a member is part of the multicast group and will not receive
multicast messages from other groups. By setting the multicast-group or the multicast-port, you
can have separate Hazelcast clusters within the same network, so it is a best practice to use
separate groups if the same network is used for different purposes. The multicast group IP address
doesnt conflict with normal unicast IP addresses since they have a specific range that is excluded
from normal unicast usage: 224.0.0.0 to 239.255.255.255 (inclusive) and defaults of 224.2.2.3. The
address 224.0.0.0 is reserved and should not be used.
2. multicast-port: The port of the multicast socket where the Hazelcast member listens and where it
sends discovery messages. Unlike normal unicast sockets where only a single process can listen to a
port, with multicast sockets multiple processes can listen to the same port. You dont need to worry
that multiple Hazelcast members running on the same JVM will conflict. This property defaults to
54327.
3. multicast-time-to-live: Sets the default time-to-live for multicast packets sent out to control the
scope of the multicasts. Defaults to 32. The maximum is 255.
4. multicast-timeout-seconds: Specifies the time in seconds that a node should wait for a valid
multicast response from another node running in the network before declaring itself as master
node and creating its own cluster. This applies only to the start-up of nodes where no master has
been assigned yet. If you specify a high value such as 60 seconds, it means until a master is selected,
each node is going to wait 60 seconds before continuing. Be careful with providing a high value.
Also avoid setting the value too low since nodes could give up too early and create their own
cluster. This property defaults to 2 seconds.
Below you can see a full example of the configuration.
<network>
<join>
<multicast enabled="true">
<multicast-group>224.2.2.3</multicast-group>
<multicast-port>54327</multicast-port>
<multicast-time-to-live>32</multicast-time-to-live>
<multicast-timeout-seconds>2</multicast-timeout-seconds>
</multicast>
</join>
</network>
<network>
<join>
<multicast enabled="true">
<trusted-interfaces>
<interface>192.168.1.104</interface>
</trusted-interfaces>
</multicast>
</join>
</network>
Hazelcast supports a wildcard on the last octet of the IP address, such as 192.168.1.*, and also supports
an IP range on the last octet, such as 192.168.1.100-110. If you do not specify any trusted-interfaces, so
the set of trusted interfaces is empty, no filtering will be applied.
If you have configured trusted interfaces but one or more nodes are not joining a cluster, your trusted
interfaced configuration may be too strict. Hazelcast will log on the finest level if a message is filtered
out so you can see what is happening.
If you use the programmatic configuration, the trusted interfaces are called trusted members.
<network>
<join>
<multicast enabled="false"/>
<tcp-ip enabled="true"/>
</join>
</network>
<network>
<join>
<tcp-ip enabled="true">
<member>192.168.1.104</member>
</tcp-ip>
</join>
</network>
You can configure multiple members using a comma separated list, or with multiple <member> entries.
You can define a range of IPs using the syntax 192.168.1.100-200. If no port is provided, Hazelcast will
automatically try the ports 5701..5703. If you do not want to depend on IP addresses, you can provide
the hostname. Instead of using more than one <member> to configure members, you can also use
<members>.
<network>
<join>
<tcp-ip enabled="true">
<members>192.168.1.104,192.168.1.105</members>
</tcp-ip>
</join>
</network>
This is very useful in combination with XML variables (see Learning The Basics: Variables).
By default, Hazelcast will bind (accept incoming traffic) to all local network interfaces. If this is an
unwanted behavior, you can set the hazelcast.socket.bind.any to false. In that case, Hazelcast will first
use the interfaces configured in the interfaces/interfaces to resolve one interface to bind to. If none is
found, Hazelcast will use the interfaces in the tcp-ip/members to resolve one interface to bind to. If no
interface is found, it will default to localhost.
When a large number of IPs are listed and members cant build up a cluster, you can set the
connection-timeout-seconds attribute, which defaults to 5, to a higher value. You can configure first
scan and delay between scans using the property hazelcast.merge.first.run.delay.seconds and
respectively hazelcast.merge.next.run.delay.seconds. By default, Hazelcast will scan every 5 seconds.
Required Member
If a member needs to be available before a cluster is started, there is an option to set the required
member:
<tcp-ip enabled="true">
<required-member>192.168.1.104</required-member>
<member>192.168.1.104</member>
<member>192.168.1.105</member>
</tcp-ip>
In this example, a cluster will only start when member 192.168.1.104 is found. Once this member is
found, it will become the master. That means required-member is the address of the expected master
node.
<network>
<join>
<aws enabled="true">
<access-key>my-access-key</access-key>
<secret-key>my-secret-key</secret-key>
</aws>
</join>
</network>
my-access-key and my-secret-key need to be replaced with your access key and secret key. Make sure
that the started machines have a security group where the correct ports are opened (see Firewall). And
also make sure that the enabled="true" section is added because if you dont add it, the AWS
configuration will not be picked up (it is disabled by default). To prevent hardcoding the access-key
and secret-key, you could have a look at Learning the Basics: Variables.
The AWS section has a few configuration options.
region: Region where the machines are running. Defaults to us-east-1. If you run in a different
region, you need to specify it, otherwise the members will not discover each other.
tag-key,tag-value: Allows you to limit the numbers of EC2 instances to look at by providing them
with a unique tag-key/tag-value. This makes it possible to create multiple clusters in a single data
center.
security-group-name: Just like the tag-key,tag-value, it filters out EC2 instances. This doesnt need to
be specified.
host-header: You can give an entry point URL for your web service using this property. It is optional.
The aws tag accepts an attribute called conn-timeout-seconds. The default value is 5 seconds. You can
increase it if you have many IPs listed and members can not properly build up the cluster.
In case you are using a different cloud provider than Amazon EC2, you can still use Hazelcast. You can
use the programmatic API to configure a TCP/IP cluster. The well known members need to be retrieved
from your cloud provider (for example, using JClouds).
If you have problems connecting and you are not sure if the EC2 instances are being found correctly,
then you could have a look at the AWSClient class. This client is used by Hazelcast to determine all the
private IP addresses of EC2 instances you want to connect to. If you feed it the configuration settings
that you are using, you can see if the EC2 instances are being found.
<hazelcast>
<partition-group enabled="true" group-type="HOST_AWARE"/>
</hazelcast>
Using this configuration, all members that share the same hostname/host IP will be part of the same
group and therefore will not host both master and backup(s). Another reason partition groups can be
useful is that normally Hazelcast considers all machines to be equal and therefore will distribute the
partitions evenly. But in some cases machines are not equal, such as different amounts of memory
available or slower CPUs, and that could lead to a load imbalance. With a partition group, you can
make member groups where each member-group has the same capacity and where each member has the
same capacity as the other members in the same member-group. In the future, perhaps a balance factor
will be added to relax these constraints. Here is an example where we define multiple member groups
based on matching IP addresses.
<hazelcast>
<partition-group enabled="true" group-type="CUSTOM">
<member-group>
<interface>10.10.1.*</interface>
</member-group>
<member-group>
<interface>10.10.2.*</interface>
</member-group
</partition-group>
</hazelcast>
In this example, there are two member groups, where the first member-group contains all member with
an IP 10.10.1.0-255 and the second member-group contains all member with an IP of 10.10.2.0-255. You
can use this approach to create different groups for each data center so that when the primary data
center goes offline, the backup data center can take over.
<hazelcast>
<group>
<name>application1</name>
<password>somepassword</password>
</group>
</hazelcast>
The password is optional and defaults to dev-pass. A group is something other than a partition-group;
with the former you create isolated clusters and with the latter you control how partitions are being
mapped to members. If you dont want to have a hard coded password, you could have a look at
Learning the Basics: Variables.
11.8. SSL
In a production environment, you often want to prevent the communication between Hazelcast
members from being tampered with or being read by an intruder because the communication could
contain sensitive information. Hazelcast provides a solution for that: SSL encryption.
The basic functionality is provided by the SSLContextFactory interface and it is configurable through
the the SSL section in network configuration. Hazelcast provides a default implementation called the
BasicSSLContextFactory which we are going to use for the example.
<network>
<join>
<multicast enabled="true"/>
</join>
<ssl enabled="true">
<factory-class-name>
com.hazelcast.nio.ssl.BasicSSLContextFactory
</factory-class-name>
<properties>
<property name="keyStore">keyStore.jks</property>
<property name="keyStorePassword">password</property>
</properties>
</ssl>
</network>
The keyStore is the path to the keyStore and the keyStorePassword is the password of the keystore. In the
example code, you can find an already created keystore; you can also find how to create one yourself
in the documentation. When you start a member, you will see that SSL is enabled.
way
you
can
configure
the
keyStore
and
keyStorePassword
is
through
the
11.9. Encryption
Apart from supporting SSL, Hazelcast also supports symmetric encryption based on the Java
Cryptography Architecture (JCA). The main advantage of using the latter is that it is easier to set up
because you dont need to deal with the keystore. The main disadvantage is that it is less secure
because SSL relies on an on-the-fly created public/private key pair and the symmetric encryption relies
on a constant password/salt.
SSL and symmetric encryption solutions have roughly the same CPU and network bandwidth overhead
because for the main data they rely on symmetric encryption; only the public key is encrypted using
asymmetric encryption. Compared to non-encrypted data, the performance degradation will be
roughly 50%. To demonstrate the encryption, lets have a look at the following configuration.
<network>
<join>
<multicast enabled="true"/>
</join>
<symmetric-encryption enabled="true">
<algorithm>PBEWithMD5AndDES</algorithm>
<salt>somesalt</salt>
<password>somepass</password>
<iteration-count>19</iteration-count>
</symmetric-encryption>
</network>
When we start two members using this configuration, well see that the symmetric encryption is
activated.
than one network interface so you may want to list the valid IPs. You can use range characters ( and -)
for simplicity. For instance, 10.3.10. refers to IPs between 10.3.10.0 and 10.3.10.255. Interface
10.3.10.4-18 refers to IPs between 10.3.10.4 and 10.3.10.18 (4 and 18 included). If network interface
configuration is enabled (it is disabled by default) and if Hazelcast cannot find an matching interface,
then it will print a message on the console and won`t start on that member.
<hazelcast>
<network>
<interfaces enabled="true">
<interface>10.3.16.*</interface>
<interface>10.3.10.4-18</interface>
<interface>192.168.1.3</interface>
</interfaces>
</network>
</hazelcast>
This is the same configuration done in programmatic way:
11.11. Firewall
When a Hazelcast member connects to another Hazelcast member, it binds to server port 5701 (see the
port configuration section) to receive the inbound traffic. On the client side also, a port needs to be
opened for the outbound traffic. By default, this will be an ephemeral port since it doesnt matter which
port is being used as long as the port is free. The problem is that the lack of control on the outbound
port can be a security issue, because the firewall needs to expose all ports for outbound traffic.
Luckily, Hazelcast is able to control the outbound ports. For example, if we want to allow the port
range 30000-31000, we can configure like this:
<network>
<join>
<multicast enabled="true"/>
</join>
<outbound-ports>
<ports>30000-31000</ports>
</outbound-ports>
</network>
To demonstrate the outbound ports configuration, start two Hazelcast members with this
configuration. When the members are fully started, execute sudo lsof -i | grep java. Below you can
see the cleaned output of that command:
java
java
java
java
46117
46117
46120
46120
IPv4
IPv4
IPv4
IPv4
TCP
TCP
TCP
TCP
*:5701 (LISTEN)
172.16.78.1:5701->172.16.78.1:30609 (ESTABLISHED)
*:5702 (LISTEN)
172.16.78.1:30609->172.16.78.1:5701 (ESTABLISHED)
There are 2 java processes, 46117 and 46120, that listen to ports 5701 and 5702 (inbound traffic). You
can see that java process 46120 uses port 30609 for outbound traffic.
Apart from specifying port ranges, you can also specify individual ports. You can combine multiple
port configurations either by separating them with commas or by providing multiple <ports> sections.
If you want to use port 30000, 30005 and port range 31000 till 32000, you could say the following:
<ports>30000,30005,31000-32000</ports>.
11.11.1. iptables
If you are using iptables, the following rule can be added to allow for outbound traffic from ports
33000-31000:
iperf -s -p 5701
Single TCP/IP connection: There is only a single TCP/IP connection between two members, not two. Also,
between client and member, there is a single TCP/IP connection. If you run the following program:
tcp6
28420/java
tcp6
28420/java
tcp6
28420/java
tcp6
28420/java
0 :::5701
:::*
LISTEN
0 :::5702
:::*
LISTEN
0 192.168.1.100:5701
192.168.1.100:57220
ESTABLISHED
0 192.168.1.100:57220
192.168.1.100:5701
ESTABLISHED
<network>
<join><multicast enabled="true"/> </join>
</network>
<services>
<service enabled="true">
<name>CounterService</name>
<class-name>CounterService</class-name>
</service>
</services>
You can see that two properties are set.
1. name: This needs to be a unique name because it will be used to look up the service when a remote
call is made. In our case, well call it CounterService. Please realize that this name will be sent with
every request, so the longer the name, the more data needs to be (de)serialized and sent over the
line. Dont make it too long, but also dont reduce it to something that is not understandable.
2. class: Class of the service, in this case, CounterService. The class needs to have a no-arg constructor,
otherwise the object cant be initialized.
We also enabled multicast discovery since well rely on that later.
You can also pass properties, which will be passed to the init method. You can do this using the
following syntax:
<service enabled="true">
<name>CounterService</name>
<class-name>CounterService</class-name>
<properties>
<someproperty>10</someproperty>
</properties>
</service>
If
you
want
to
parse
more
complex
XML,
you
might
want
to
have
look
at
the
com.hazelcast.spi.ServiceConfigurationParser which will give you access to the XML DOM tree.
Of course, we want to see this in action.
CounterService.init
The CounterService is started as part of the startup of the HazelcastInstance. If you shutdown the
HazelcastInstance, for example, by using Control-C, then you will see:
CounterService.shutdown
12.2. Proxy
In the previous section, we created a CounterService that starts when Hazelcast starts, but apart from
that it doesnt do anything yet. In this section, we connect the Counter interface to the CounterService,
we do a remote call on the member hosting the eventual counter data/logic, and we return a dummy
result. In Hazelcast, remoting is done through a Proxy: on the client side, you get a proxy which
exposes your methods. When a method is called, the proxy creates an operation object, sends this
operation to the machine responsible to execute that operation, and eventually sends the result.
First, we let the Counter implement the DistributedObject interface to indicate that it is a distributed
object. Some additional methods will be exposed, such as getName, getId, and destroy.
next
step
is
enhancing
com.hazelcast.spi.ManagedService
the
CounterService.
interface,
it
Apart
now
from
also
implementing
the
implements
the
com.hazelcast.spi.RemoteService interface. Through this interface, a client can get a handle of a Counter
proxy.
InvocationBuilder based on the operation and the partitionId using the InvocationBuilder. This is
where the connection is made between the operation and the partition.
The last part is invoking the Invocation and waiting for its result. This is done using a Future, which
gives us the ability to synchronize on completion of that remote executed operation and to get the
results. In this case, we do a simple get since we dont care about a timeout; for real systems, it is often
better to use a timeout since most operations should complete in a certain amount of time. If they dont
complete, it could be a sign of problems; waiting indefinitely could lead to stalling systems without any
form of error logging.
If the execution of the operation fails with an exception, an ExecutionException is thrown and needs to
be dealt with. Hazelcast provides a utility function for that: ExceptionUtil.rethrow(Throwable). If you
want to keep the checked exception, you need to deal with exception handling yourself, and the
ExceptionUtil is not of much use. A nifty improvement for debugging is that if a remote exception is
thrown, the stacktrace includes the remote side and the local side. This makes it possible to figure out
what went wrong on both sides of the call.
If the exception is an InterruptedException, you can do two things. Either propagate the
InterruptedException since it is a good practice for blocking methods like shown below, or just use the
ExceptionUtil.rethrow for all exceptions.
try {
final Future<Integer> future = invocation.invoke();
return future.get();
} catch(InterruptedException e){
throw e;
} catch(Exception e){
throw ExceptionUtil.rethrow(e);
}
In this case, we dont care about the InterruptedException and therefore we catch all exceptions and let
them be handled by the ExceptionUtil: it will be wrapped in a HazelcastException and the interrupt
status will be set.
Currently, it isnt possible to abort an operation by calling the future.cancel method. Perhaps this will
be added in a later release. This is also the reason why Executor Futures are not working since the
executor is built on top of the SPI.
Lets do the part of the example that has been missing so far: the IncOperation.
Another important part of the IncOperation is that it implements the PartitionAwareOperation interface.
This is an indicator for the OperationService that this operation should be executed on a certain
partition. In our case, the IncOperation should be executed on the partition hosting our counter.
Because the IncOperation needs to be serialized, the writeInternal and readInternal methods need to be
overwritten so that the objectId and amount are serialized and will be available when this operation
runs. For deserialization, it is also mandatory that the operation has a no-arg constructor.
Of course, we want to run the code.
Executing
0
Executing
0
Executing
0
Executing
0
Finished
We can see that our counters are being stored in different members (check the different port
numbers). We can also see that the increment does not do any real logic yet since the value remains at
0. We will solve this in the next section.
In this example we managed to get the basics up and running, but some things are not correctly
implemented. For example, in the current code, a new proxy is always returned instead of a cached
one. Also, the destroy is not correctly implemented on the CounterService. In the following examples,
these issues will be resolved.
12.3. Container
In this section, we upgrade the functionality so that it features a real distributed counter. Some kind of
data structure will hold an integer value and can be incremented, and we will also cache the proxy
instances and deal with proxy instance destruction.
The first thing we do is that for every partition in the system, we create a Container which will contain
all counters and proxies for a given partition.
createDistributedObject method; apart from creating the proxy, we also initialize the value for that
given proxy to 0, so that we dont run into a NullPointerException. In the destroyDistributedObject
method, the value for the object is removed. If we dont clean up, well end up with memory that isnt
removed and that can potentially can lead to an OOME.
The last step is connecting the IncOperation.run to the container.
Round 1
Executing
1
Executing
1
Executing
1
Executing
1
Round 2
Executing
2
Executing
2
Executing
2
Executing
2
Finished
This means that we now have a basic distributed counter up and running!
class Container {
void clear() {
values.clear();
}
void applyMigrationData(Map<String, Integer> migrationData) {
values.putAll(migrationData);
}
Map<String, Integer> toMigrationData() {
return new HashMap(values);
}
...
}
1. toMigrationData: This method is called when Hazelcast wants to start the migration of the partition
on the member that currently owns the partition. The result of the toMigrationData is partition data
in a form that can be serialized to another member.
2. applyMigrationData: This method is called when the migrationData that is created by the
toMigrationData method is applied to a member that is going to be the new partition owner.
3. clear: This method is called for two reasons. One reason is when the partition migration has
succeeded and the old partition owner can get rid of all the data in the partition. The other reason
is when the partition migration operation fails and the new partition owner needs to roll back its
changes.
The next step is to create a CounterMigrationOperation that will be responsible for transferring the
migrationData from one machine to anther and to call the applyMigrationData on the correct partition of
the new partition owner.
one backup.
commitMigration: This method commits the migrated data. In this case, committing means that we
clear the container for the partition of the old owner. Even though we dont have any complex
resources like threads, database connections, etc., clearing the container is advisable to prevent
memory issues. This method is called on both the primary and the backup. If this node is on the
source side of migration (partition is migrating FROM this node) and the migration type is MOVE
(partition is migrated completely, not copied to a backup node), then the method removes partition
data from this node. If this node is the destination or the migration type is copy, then the method
does nothing. If this node is on the destination side of migration (partition is migrating TO this
node) then this method removes partition data from this node. If this node is on the source, then
the methods does nothing.
rollbackMigration: Rolls back a migration.
12.5. Backups
In this last section, we deal with backups; we make sure that when a member fails, then the data of the
counter is available on another node. This is done by replicating that change to another member in the
cluster.
With
the
SPI,
you
can
do
this
by
letting
the
operation
implement
the
com.hazelcast.spi.BackupAwareOperaton interface. Below, you can see this interface being implemented
on the IncOperation.
you
process
lot
of
events
and
you
have
many
cores,
changing
the
value
of
hazelcast.event.thread.count property to a higher value is a good idea. This way, more events can be
processed in parallel.
Multiple components share the same event queues. If there are 2 topics, say A and B, for certain
messages they may share the same queue(s) and hence the same event thread. If there are a lot of
pending messages produced by A, then B needs to wait. Also, when processing a message from A takes
a long time and the event thread is used for that, B will suffer from this. That is why it is better to
offload processing to a dedicated thread (pool) so that systems are better isolated.
If events are produced at a higher rate than they are consumed, the queue will grow in size. To prevent
overloading the system and running into an OutOfMemoryException, the queue is given a capacity of 1
million items. When the maximum capacity is reached, the items are dropped. This means that the
event system is a "best effort" system. There is no guarantee that you are going to get an event. It can
also be that Topic A has a lot of pending messages, and therefore B cannot receive messages because
the queue has no capacity and messages for B are dropped. Another reason events are not reliable is
that the JVM of the receiver crashes, all the messages in the event- queues will be lost.
that
are
not
partition
IExecutorService.executeOnMember(command,member) operation.
aware,
such
as
the
Each of these types has a different threading model that is explained below.
threads available.
Example:
Take a 3 node cluster. Two members will have 90 primary partitions and one member will have 91
primary partitions. Lets say you have one CPU and 4 cores per CPU. By default, 8 operation threads
will be allocated to serve 90 or 91 partitions.
non-partition-aware
operations,
next
to
the
genericWorkQueue,
there
also
is
13.7. Queries
Unlike regular operations such as IMap.get, which run on operation threads, there is also a group of
operations that do not run on partition threads, but run on query threads: for example, the
Collection<V> IMap.values(Predicate predicate) method. This means there is a separate thread pool
that executes queries so queries can be run in parallel with regular operations. You can influence the
size of the query thread pool by creating an ExecutorConfig (either programmatically or through XML)
for the hz:query executor.
this executor, you can create an executor with the name hz:map-load and fine tune, for example, the
thread pool size.
Unlike regular partition operations, the MapLoader.loadAll(keys) is not executed on a partition thread.
This means that regular operations for that partition that access different data structures, instead of
being blocked, can still be executed.
functionality
can
be
turned
on/off
by
using
the
system
property
log
size
small.
If
logging
of
stacktraces
is
enabled
(using
Defines
the
retention
time
of
invocations in slow operation logs. If an invocation is older than this value, it will be purged from
the log to prevent unlimited memory usage. When all invocations are purged from a log, the log
itself will be deleted.
The purging removes each invocation whose retention time is exceeded. When all invocations are
purged from a slow operation log, the log is deleted.
14.6. Other
The SystemLogServices retains some logging information that you can use with the Management Center.
If you are not using it, you can disable it by setting hazelcast.system.log.enabled to false.
We are going to create two ubuntu servers at once. Enter 2 for "Number of instances" and press the
"Continue" button.