Introducing Redisson Live Objects (Object Hash Mapping)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
视频直播,500GB 1个月
简介: 文章来源于阿里云 MVP顾睿。

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.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.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 DequeListQueue, 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 to JsonJacksonCodec 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 to IMPLEMENTATION_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.

文章转载自阿里云 MVP顾睿,查看原文

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4月前
|
SQL 存储 数据库
对象关系映射(Object-Relational Mapping)
【8月更文挑战第17天】
102 2
|
5月前
|
开发者 Python
【Python】已解决:TypeError: descriptor ‘index‘ for ‘list‘ objects doesn‘t apply to a ‘str‘ object
【Python】已解决:TypeError: descriptor ‘index‘ for ‘list‘ objects doesn‘t apply to a ‘str‘ object
151 0
|
存储 NoSQL Java
Java常用API(一)Object与Objects
Java常用API(一)Object与Objects
68 0
|
算法 数据可视化 机器人
Object SLAM: An Object SLAM Framework for Association, Mapping, and High-Level Tasks 论文解读
Object SLAM: An Object SLAM Framework for Association, Mapping, and High-Level Tasks 论文解读
97 0
|
缓存 JavaScript 前端开发
NestJS:理解ORM(Object Relational Mapping)
NestJS:理解ORM(Object Relational Mapping)
144 0
|
Java 中间件 数据安全/隐私保护
本地使用sentinel 到界面;Object类与Objects类
本地使用sentinel 到界面 Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。 1/https://github.com/alibaba/Sentinel/releases 下载jar
100 1
本地使用sentinel 到界面;Object类与Objects类
Object类的toString和Equals方法,以及Objects类的Equals方法
Object类 toString()方法 public class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name =
|
3天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
133 4
|
2月前
|
Java
Java Object 类详解
在 Java 中,`Object` 类是所有类的根类,每个 Java 类都直接或间接继承自 `Object`。作为所有类的超类,`Object` 定义了若干基本方法,如 `equals`、`hashCode`、`toString` 等,这些方法在所有对象中均可使用。通过重写这些方法,可以实现基于内容的比较、生成有意义的字符串表示以及确保哈希码的一致性。此外,`Object` 还提供了 `clone`、`getClass`、`notify`、`notifyAll` 和 `wait` 等方法,支持对象克隆、反射机制及线程同步。理解和重写这些方法有助于提升 Java 代码的可读性和可维护性。
112 20