Introducing Redisson Live Objects (Object Hash Mapping)
What is Redisson?
Redisson is a Redis Java library that provides distributed Java objects and services including Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service and most recently added Scheduled executor service on top of the Redis server.
What is a Live Object?
A Live Object can be understood as an enhanced version of standard Java object, of which an instance reference can be shared not only between threads in a single JVM, but can also be shared between different JVMs across different machines. Wikipedia discribes it as:
Live distributed object (also abbreviated as live object) refers to a running instance of a distributed multi-party (or peer-to-peer) protocol, viewed from the object-oriented perspective, as an entity that has a distinct identity, may encapsulate internal state and threads of execution, and that exhibits a well-defined externally visible behavior.
How Redisson Live Object Works
Redisson Live Object (RLO) realized this idea by mapping all the fields inside a Java class to a Redis hash through a runtime-constructed proxy class. All the get/set methods of each field are translated to hget/hset commands operated on the Redis hash, making it accessable to/from any clients connected to the same Redis server. As we all know, the field values of an object represent its state; having them stored in a remote repository, Redis, makes it a distributed object. This object is a Redisson Live Object.
What Benefits Does it Have Over a Standard Java Object?
This process provides a range of benefit over standard Java objects. Traditionally, when multiple threads access a shared object, the reference of the object is then passed to each of them. This model is fine for standalone applications, but it's not possible to share the object between applications and/or servers in a distributed environment. For those types of use cases, we would have to serialize the object, transmit it to the other end and deserialize it back to an object. It is often the case that the entire object has to be serialized and transmitted even just one field is modified. This process not only adds performance overhead to the application, it also complicates the programming model: A serialized object is detached from its original, so changes to the state of the object which happen after serialisation are not visible to the other end.
By using RLO, sharing an object between applications and/or servers is the same as sharing one in a standalone application. This removes the need for serialization and deserialization, and at the same time reduces the complexity of the programming model: Changes made to one field is (almost^) immediately accessable to other processes, applications and servers. (^Redis' eventual consistant replication rule still applies when connected to slave nodes)
Since the Redis server is a single-threaded application, all field access to the live object is automatically executed in atomic fashion: a value will not be changed when you are reading it.
With Redisson Live Object, you can treat the redis server as a shared Heap space for all connected JVMs.
How is a Live Object Different Than a Standard Java Object?
Due to the fact that the state of the object is kept in Redis instead of in the local JVM, there are a few differences compared to using standard Java objects, and they need to be taken into consideration when architecting a system that employs this feature.
As described above, in a Redisson Live Object, changes to its fields are immediately made available to other processes and JVMs. This in effect makes all fields in the object volatile.
What Kind of Application Can Benefit From It?
Redisson Live Object can prove itself to be useful in many domains and fields. It can assist developers in easily creating team-collabrating systems, such as a whiteboard, mobile game matchmaking, cross device copy-and-paste like you've seen in the upcoming iOS 10, and many others.
It can be used to improve availability of a system or service while reducing the complexity of its implementation. Having critical system states and/or configurations stored as live objects in a redis cluster, the system can survive from node crashes without complicated logic to achieve background state syncing and config validating upon reviving. This is extremely useful for mission critical systems in the Oil and Gas industry, Energy industry, Banking/Finance industry, and many more.
It can also be used to manage the state of swarms of connected devices. By connecting multiple devices together to a shared live object, we can easily make dumb switches and devices into a smart connected self-aware network.
While the Redisson Live Object seems incredibly useful on its own, with careful design and engineering, it can be used to create some features which are even greater. By combining it with Redisson Remote Service, we can create applications that can be paused, fast-forwarded and even rewound and replayed on demand. By constructing all the application states as live objects, you can kill all running process and have the application "Paused"; Or simply run multiple copies of the same application on multiple nodes to reduce the load on each individual node and/or increase the overall performance of the system. This effectively makes the system go "Fast-forward"; Or you can keep snapshots of all the live objects every so often, while keeping all the changes tracked in a log, you can have the application go back in time, relive a moment in the past and replay all the changes as you wish. This is making the system "Rewind and Replay". It's like elastic computing happening at the application level.
How Does it Differ From Other Live Objects?
Redisson Live Object was designed and developed by Rui Gu overseen by Nikita Koksharov. The idea was inspired originally by the Java JPA API since both the RDBMs and redis are centralized services and used as data repositories. It was initially called "Attached Object" since its behavior was somewhat similar to the JPA entity object inside a JPA transaction.
Nikita Koksharov suggested the name "Live Object" and we both think it is more appropriate for the final shape of the feature than "Attached Object".
Knowing the evolution of this feature, it is understandable that while sharing the same Live Object design pattern with solutions from other projects, Redisson Live Object works differently internally to those solutions in a few ways. Redisson Live Object uses redis as its data storage, all changes to the object are translated as redis commands and operated on a given redis hash. The local JVM does not hold any value in the fields of the object except for the field that represents the key name of the hash. Other solutions are focussed on retaining a local copy of the object at each JVM and transmitting all the changes to the object over a shared pub/sub channel to each other participant. It is obvious that the solution used for Redisson Live Objects is superior compared with the solutions from other projects. While other solutions have employed complicated mechanisms to guarantee each participant receives and reflects all the changes in same order, Redisson naturally inherits this guarantee from redis because it is a single-threaded centralised service. Commands sent to it are always processed in a first-come, first-served manner. Because the Redisson Live Object keeps its state in a remote repository, the state can be retained even after all of the participants go offline, whereas other solutions require at least one participant to stay online at any time to ensure the state of the objects are not lost.
Usage
In order to enjoy all the benefits brought by Redisson Live Object, only thing you need to do is annotate the Class you desire to use with @REntity, then annotate a field with @RId.
@REntity
public class MyLiveObject {
@RId
private String name;
//other fields
...
...
//getters and setters
...
...
}
Now you have made an otherwise standard Java object class into a Redisson Live Object class. You are able to get an instance of it with the RedissonLiveObjectService
, which you can get from a RedissonClient.
...
RLiveObjectService service = redisson.getLiveObjectService();
MyLiveObject myObject1 = service.<MyLiveObject, String>getOrCreate(MyLiveObject.class, "myObjectId");
...
Using the Redisson Live Object is the same as using a standard Java object. Let's assume you have this object:
@REntity
public class MyObject {
@RId
private String name;
private String value;
public MyObject(String name) {
this.name = name;
}
public MyObject() {
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
}
Somewhere else in the code you may want to create it as a standard Java instance.
//Standard Java object instance
MyObject standardObject1 = new MyObject();
standardObject1.setName("standard1");
//Of course you can use non-default constructor
MyObject standardObject2 = new MyObject("standard2");
Elsewhere, you may also want to create it as a Redisson Live Object instance:
//first create the service
RLiveObjectService service = redisson.getLiveObjectService();
//instantiate the object with the service
MyObject liveObject1 = service.<MyObject, String>getOrCreate(MyObject.class, "liveObjectId");
//Behind scense, it tries to locate the constructor with one argument and invoke with the id value,
//"liveObjectId" in this case. If the constructor is not found, falls back on default constructor
//and then call setName("liveObjectId") before returns back to you.
There is literally no difference when it comes to using these instances:
//Setting the "value" field is the same
standardObject1.setValue("abc");//the value "abc" is stored in heapspace in VM
standardObject2.setValue("abc");//same as above
liveObject1.setValue("abc");
//the value "abc" is stored inside redis, no value is stored in heap. (OK, there
//is a string pool, but the value is not referenced here in the object, so it can
//be garbage collected.)
//Getting the "value" out is just the same
System.out.println(standardObject1.getValue());
//It should give you "abc" in the console, the value is retrieved from heapspace in the VM;
System.out.println(standardObject2.getValue());//same as above.
System.out.println(liveObject1.getValue());
//output is the same as above, but the value is retrieved from redis.
While these two snippets of code look exactly the same, there is a slight difference between them. Let me explain it with another example:
@REntity
public class MyLiveObject {
@RId
private String name;
private MyOtherObject value;
public MyLiveObject(String name) {
this.name = name;
}
public MyObject() {
}
public String getName() {
return name;
}
public MyOtherObject getValue() {
return value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(MyOtherObject value) {
this.value = value;
}
}
In this case, the type of the "value" field is a mutable type. In a standard Java object, when you invoke the getValue()
method, the reference to this MyOtherObject
instance is returned to you. When you invoke the same method on a Redisson Live Object, a reference of a new instance is returned. This can have the following two effects:
//Redisson Live Object behaviour:
MyLiveObject myLiveObject = service.getOrCreate(MyLiveObject.class, "1");
myLiveObject.setValue(new MyOtherObject());
System.out.println(myLiveObject.getValue() == myLiveObject.getValue());
//False (unless you use a custom Codec with object pooling)
//Standard Java Object behaviour:
MyLiveObject notLiveObject = new MyLiveObject();
notLiveObject.setValue(new MyOtherObject());
System.out.println(notLiveObject.getValue() == notLiveObject.getValue());
//True
//Redisson Live Object behaviour:
MyLiveObject myLiveObject = service.getOrCreate(MyLiveObject.class, "1");
MyOtherObject other = new MyOtherObject();
other.setOtherName("ABC");
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//ABC
other.setOtherName("BCD");
System.out.println(myLiveObject.getValue().getOtherName());
//still ABC
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//now it's BCD
//Standard Java Object behaviour:
MyLiveObject myLiveObject = service.getOrCreate(MyLiveObject.class, "1");
MyOtherObject other = new MyOtherObject();
other.setOtherName("ABC");
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//ABC
other.setOtherName("BCD");
System.out.println(myLiveObject.getValue().getOtherName());
//already is BCD
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//still is BCD
The reason for this difference in behavior is because we are not keeping any of the object states, and each setter and getter call will serialize and deserialize the value to and from Redis back to a local VM. This effectively detaches the field value from the object state. This behavior is usually not a problem when the value type is an immutable type, such as String, Double, Long, etc. When you are dealing with a mutable type, you may want to benefit from this behaviour, since the value instance is detached off from the object state, you can consider all the read/write actions to this value instance is effectively in a transaction with ACID property. It can be extremely useful when the application is designed to incorporate this behavior properly. If you prefer to stick to standard Java behavior, you can always convert the MyOtherObject
into a Redisson Live Object.
//Redisson Live Object with nested Redisson Live Object behaviour:
MyLiveObject myLiveObject = service.getOrCreate(MyLiveObject.class, "1");
MyOtherObject other = service.getOrCreate(MyOtherObject.class, "2");
other.setOtherName("ABC");
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//ABC
other.setOtherName("BCD");
System.out.println(myLiveObject.getValue().getOtherName());
//you see, already is BCD
myLiveObject.setValue(other);
System.out.println(myLiveObject.getValue().getOtherName());
//and again still is BCD
Field types in the Redisson Live Object can be almost anything, from Java util classes to collection/map types and of course your own custom objects, as long as it can be encoded and decoded by a supplied codec. More details about the codec can be found in the Advanced Usage section.
As much as I like to say it's free with no limits, there are still some restrictions on the choices of field types you can have. The field annotated with RId
can not be an array type, i.e. int[], long[], double[], byte[], etc. More details and explainations can be found in Restrictions section
In order to keep Redisson Live Objects behaving as closely to standard Java objects as possible, Redisson automatically converts the following standard Java field types to its counter types supported by Redisson RObject
.
Standard Java Class | Converted Redisson Class |
---|---|
SortedSet.class | RedissonSortedSet.class |
Set.class | RedissonSet.class |
ConcurrentMap.class | RedissonMap.class |
Map.class | RedissonMap.class |
BlockingDeque.class | RedissonBlockingDeque.class |
Deque.class | RedissonDeque.class |
BlockingQueue.class | RedissonBlockingQueue.class |
Queue.class | RedissonQueue.class |
List.class | RedissonList.class |
The conversion prefers the one nearer to the top of the table if a field type matches more than one entries. i.e. LinkedList
implements Deque
, List
, Queue
, it will be converted to a RedissonDeque
because of this.
Instances of these Redisson classes retains their states, values, and entries in Redis too, changes to them are directly reflected into Redis without keeping values in local VM.
Advanced Usage
As described before, Redisson Live Object classes are proxy classes which can be fabricated when needed and then get cached in a RedissonClient
instance against its original class. This process can be a bit slow and it is recommended to pre-register all the Redisson Live Object classes via RedissonLiveObjectService
for any kind of delay-sensitive applications. The service can also be used to unregister a class if it is no longer needed. And of course it can be used to check if the class has already been registered.
RLiveObjectService service = redisson.getLiveObjectService();
service.registerClass(MyClass.class);
service.unregisterClass(MyClass.class);
Boolean registered = service.isClassRegistered(MyClass.class);
@REntity
The behaviour of each type of Redisson Live Object can be customised through properties of the @REntity
annotation. You can specify each of those properties to gain fine control over its behaviour.
- namingScheme - You can specify a naming scheme which tells Redisson how to assign key names for each instance of this class. It is used to create a reference to an existing Redisson Live Object and materialising a new one in redis. It defaults to use Redisson provided
DefaultNamingScheme
. - codec - You can tell Redisson which
Codec
class you want to use for the Redisson Live Object. Redisson will use an instance pool to locate the instance based on the class type. It defaults toJsonJacksonCodec
provided by Redisson. - fieldTransformation - You can also specify a field transformation mode for the Redisson Live Object. As mentioned before, in order to keep everything as close to standard Java as possible, Redisson will automatically transform fields with commonly-used Java util classes to Redisson compatible classes. This uses
ANNOTATION_BASED
as the default value. You can set it toIMPLEMENTATION_BASED
which will skip the transformation.
@RId
The @RId
annotation is used on a field that can be used to distinguish between one instance and another. Think of this field as the primary key field of this class. The value of this field is used to create a reference to existing Redisson Live Object. The field with this annotation is the only field that has its value also kept in the local VM. You can only have one RId
annotation per class.
You can supply a generator
strategy to the @RId
annotation if you want the value of this field to be programatically generated. The default generator is RandomUUIDIdStringGenerator
which generates a v4(Random) UUID string when used.
@RObjectField
When the transformationMode
in @REntity
is set to ANNOTATION_BASED
, which is the default value, you can optionally use it to annotate a field that does not have @RId
annotation at the same time. This is often used to give a different namingScheme
and/or a different codec
class to the ones specified in @REntity
.
As you can see the codec
and namingScheme
are quite often used in providing a Redisson Live Object and its service, in order to reduce the amount of redundant instances. Redisson, by default, caches these instances internally to be reused. You can supply your own providers for each of them via Redisson's Config
instance.
Restrictions
At the moment, Redisson Live Objects can only be classes with the default constructor or classes with a single-argument constructor, and the argument is assumed to be used as the value for field with RId
annotaion. As mentioned above, the type of the RId
field cannot be an Array type. This is due to the DefaultNamingScheme
which cannot serialize and deserialize the Array type as of yet. This restriction can be lifted once the DefaultNamingScheme
is improved. Since the RId
field is encoded as part of the key name used by the underlying RMap, it makes no sense to create a RLO with just have one field. It is better to use a RBucket
for this type of usage.