How to integrate Redis in Liferay for Tomcat's Session Replication

Redis is an in-memory store that can be used to provide a central and external place to save the application session information. It can be very useful in cloud environments or to handle node crashes without losing session information.

Redisson appears as an alternative in order to provide integration between Redis and Tomcat:

  • Provides a Tomcat Session manager that Stores session of Apache Tomcat in Redis and allows to distribute requests across a cluster of Tomcat servers.
  • Implements non-sticky session management backed by Redis.

It has some advantages like writing only the modified attribute instead of serializing the whole session each time. You have more information in Redisson Tomcat.

Installation

The following steps are needed for a basic installation:

  1. Add RedissonSessionManager: It can be added both on $TOMCAT_BASE/conf/context.xml or, per context, in $TOMCAT_BASE/conf/server.xml. You have more information in Redisson Tomcat.
    <Manager className="org.redisson.tomcat.RedissonSessionManager"
      configPath="${catalina.base}/redisson.conf" 
      readMode="REDIS" updateMode="DEFAULT" broadcastSessionEvents="false"/>

     

  2. Create, in ${catalina.base}, a redisson.conf with a default configuration:
    {
    "singleServerConfig":{
      "address": "redis://127.0.0.1:6379"
    },
    "threads":0,
    "nettyThreads":0,
    "transportMode":"NIO"
    }

     

  3. Copy, in $TOMCAT_BASE/lib the libraries redisson-all and redisson-tomcat-9 that can be dowloaded from Redisson Tomcat.

 

Impediments

Redisson, per design, isn't fully prepared to make use of OSGi classloader benefits, and its decoders aren't able to find the appropriate Liferay classloaders to perform a correct serialization/deserialization. With a configuration like the previous one, with FstCodec as default decoder, as soon as you start the portal you'll get an exception like the following one:

Caused by: java.lang.RuntimeException: class not found CLASSNAME:com.liferay.portal.model.impl.PortalPreferencesImpl loader:org.eclipse.osgi.internal.loader.EquinoxClassLoader@41447be5[classic-theme:4.0.3(id=949)]
        at org.nustaq.serialization.FSTClazzNameRegistry.classForName(FSTClazzNameRegistry.java:235)
        at org.nustaq.serialization.FSTClazzNameRegistry.classForName(FSTClazzNameRegistry.java:190)
        at org.nustaq.serialization.FSTClazzNameRegistry.decodeClass(FSTClazzNameRegistry.java:173)
        at org.nustaq.serialization.coders.FSTStreamDecoder.readClass(FSTStreamDecoder.java:478)
        at org.nustaq.serialization.FSTObjectInput.readClass(FSTObjectInput.java:939)
        at org.nustaq.serialization.FSTObjectInput.readObjectWithHeader(FSTObjectInput.java:347)
        at org.nustaq.serialization.FSTObjectInput.readObjectFields(FSTObjectInput.java:713)
        at org.nustaq.serialization.FSTObjectInput.instantiateAndReadNoSer(FSTObjectInput.java:566)
        at org.nustaq.serialization.FSTObjectInput.readObjectWithHeader(FSTObjectInput.java:374)
        at org.nustaq.serialization.FSTObjectInput.readObjectInternal(FSTObjectInput.java:331)
        at org.nustaq.serialization.FSTObjectInput.readObject(FSTObjectInput.java:311)
        at org.nustaq.serialization.FSTObjectInput.readObject(FSTObjectInput.java:245)
        ... 24 more
Caused by: java.lang.ClassNotFoundException: com.liferay.portal.model.impl.PortalPreferencesImpl cannot be found by classic-theme_4.0.3
        at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:508)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:419)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:411)
        at org.eclipse.osgi.internal.loader.ModuleClassLoader.loadClass(ModuleClassLoader.java:151)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:348)
        at org.nustaq.serialization.FSTClazzNameRegistry.classForName(FSTClazzNameRegistry.java:197)
        ... 35 more

 

Approaches

Some alternatives can be implemented to overcome this problem:

Solution Based on Dynamic Imports

A two step solution:

  1. Create a module able to track, dynamically, every exported package using the DynamicImport-Package directive making, afterwards, its classloader visible to Redisson.
  2. Extend Redisson to be able to use the previous classloder.

This solution is based on how Liferay solves the execution of Groovy scripts in the Control Panel: https://github.com/liferay/liferay-portal/blob/master/modules/apps/server/server-admin-web/bnd.bnd Normally this is not a recommended design since it breaks the OSGi modularity concept and creates a very heavy class path, but it's very fast to implement and requires few maintenance.

You can download the code from https://github.com/marianoalvarosaiz/liferay-utilities/tree/master/modules/redis-redisson-liferay-adapter.

How to configure it:

  1. Ensure that the generated com.liferay.redis.tomcat.client.jar is placed in $TOMCAT_HOME/lib/ext.

     

  2. Modify the configuration to be able to use the newly created codec: FstLiferayCodec. In a basic default configuration:
    {
    "singleServerConfig":{
      "address": "redis://127.0.0.1:6379"
    },
    "threads":0,
    "nettyThreads":0,
    "transportMode":"NIO",
    "codec":{
    "class":"com.liferay.redis.tomcat.client.redisson.codec.FstLiferayCodec"
    },
    }

     

  3. Deploy in $LIFERAY_HOME/deploy the module com.liferay.redis.tomcat.adapter.jar.

 

Solution Based on Extending Redisson Session Manager

Extends RedissonSessionManager to use Liferay Serializer/Deserializer. Liferay serialization handlers are based on the ClassLoaderPool that contains every module classloader, which lets it handle every class serialization/deserialization. In the end the idea is to leave Liferay handle the inner process since it knows how to reach every classloader.

This solution is based on how Liferay handles PortletSession replication, which somehow it's easier since PortletSessionImpl is a class managed by Liferay and can wrap the session every time the constructor is invoked. Probably this solution is better from an overall performance point of view, it matches OSGi principles, but it can be harder to maintain since there's a big coupling between Redisson and Liferay.

You can use the code from https://github.com/marianoalvarosaiz/liferay-utilities/tree/master/modules/redis-redisson-integration. It builds a jar that wraps redisson-tomcat and redisson-all and that must be placed in $TOMCAT_HOME/lib/ext.

How to configure it:

  1. Ensure that the generated com.liferay.redis.redisson.integration.jar is placed in $TOMCAT_HOME/lib/ext.
  2. Change the configuration to use com.liferay.redis.redisson.integration.tomcat.LiferayRedissonSessionManager instead of org.redisson.tomcat.RedissonSessionManager.
    <Manager className="com.liferay.redis.redisson.integration.tomcat.LiferayRedissonSessionManager"
      configPath="${catalina.base}/redisson.conf" 
      readMode="REDIS" updateMode="DEFAULT" broadcastSessionEvents="false"/>
Blogs

Hi Mariano, thanks for your great and useful article, I've successfully integrated Redis Session Manager using your 2nd approach (redis-redisson-integration).

I can't, however, remove this annoying log:

 org.redisson.tomcat.RedissonSessionManager.findSession Session D26F37F71B84A44A8D0EA39BC648FB79 can't be found

I tried to modify the liferay portal-log4.xml without any luck:

...

<category name="org.redisson.tomcat.RedissonSessionManager">        <priority value="ERROR" />    </category>

...

could you help me?