killbill-memoizeit

cache: Redis integration Signed-off-by: Pierre-Alexandre

10/22/2018 8:04:36 AM

Changes

account/pom.xml 5(+5 -0)

beatrix/pom.xml 5(+5 -0)

catalog/pom.xml 5(+5 -0)

currency/pom.xml 5(+5 -0)

invoice/pom.xml 5(+5 -0)

jaxrs/pom.xml 5(+5 -0)

junction/pom.xml 5(+5 -0)

overdue/pom.xml 5(+5 -0)

payment/pom.xml 5(+5 -0)

tenant/pom.xml 11(+11 -0)

usage/pom.xml 5(+5 -0)

util/pom.xml 9(+9 -0)

Details

account/pom.xml 5(+5 -0)

diff --git a/account/pom.xml b/account/pom.xml
index 331bc4f..b046788 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -64,6 +64,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

beatrix/pom.xml 5(+5 -0)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 4b30973..4385631 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -76,6 +76,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
         </dependency>

catalog/pom.xml 5(+5 -0)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index 05a0edc..6da1d43 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -62,6 +62,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
index 8e4b9c9..964fa3a 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
@@ -29,10 +29,16 @@ import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.rules.DefaultPlanRules;
+import org.redisson.client.codec.Codec;
+import org.redisson.codec.SerializationCodec;
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import io.netty.buffer.ByteBuf;
+
 public class TestVersionedCatalog extends CatalogTestSuiteNoDB {
 
     final DateTime dt0 = new DateTime("2010-01-01T00:00:00+00:00");
@@ -168,4 +174,38 @@ public class TestVersionedCatalog extends CatalogTestSuiteNoDB {
         // This would be called for instance when computing billing events (dt3 could be a future PHASE event for instance)
         vc.findPlan("shotgun-quarterly", dt3, dt1);
     }
+
+    @Test(groups = "fast")
+    public void testDefaultPlanRulesExternalizable() throws IOException {
+        final Codec codec = new SerializationCodec();
+        final ByteBuf byteBuf = codec.getValueEncoder().encode(vc.getVersions().get(0).getPlanRules());
+        final DefaultPlanRules planRules = (DefaultPlanRules) codec.getValueDecoder().decode(byteBuf, null);
+        Assert.assertEquals(planRules, vc.getVersions().get(0).getPlanRules());
+    }
+
+    @Test(groups = "fast")
+    public void testProductExternalizable() throws IOException {
+        final Codec codec = new SerializationCodec();
+        for (final Product product : vc.getVersions().get(0).getCatalogEntityCollectionProduct().getEntries()) {
+            final ByteBuf byteBuf = codec.getValueEncoder().encode(product);
+            final Product product2 = (Product) codec.getValueDecoder().decode(byteBuf, null);
+            Assert.assertEquals(product2, product);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testCatalogEntityCollectionProductExternalizable() throws IOException {
+        final Codec codec = new SerializationCodec();
+        final ByteBuf byteBuf = codec.getValueEncoder().encode(vc.getVersions().get(0).getCatalogEntityCollectionProduct());
+        final Collection products = (CatalogEntityCollection) codec.getValueDecoder().decode(byteBuf, null);
+        Assert.assertEquals(products, vc.getVersions().get(0).getCatalogEntityCollectionProduct());
+    }
+
+    @Test(groups = "fast")
+    public void testStandaloneCatalogExternalizable() throws IOException {
+        final Codec codec = new SerializationCodec();
+        final ByteBuf byteBuf = codec.getValueEncoder().encode(vc.getVersions().get(0));
+        final StandaloneCatalog standaloneCatalog = (StandaloneCatalog) codec.getValueDecoder().decode(byteBuf, null);
+        Assert.assertEquals(standaloneCatalog, vc.getVersions().get(0));
+    }
 }

currency/pom.xml 5(+5 -0)

diff --git a/currency/pom.xml b/currency/pom.xml
index 61de314..23a2846 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -47,6 +47,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 7e68eca..11e9e78 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -60,6 +60,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

invoice/pom.xml 5(+5 -0)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index 9becc1d..810f9fa 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -65,6 +65,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

jaxrs/pom.xml 5(+5 -0)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index 4ba84f6..4239397 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -71,6 +71,11 @@
             <artifactId>swagger-models</artifactId>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

junction/pom.xml 5(+5 -0)

diff --git a/junction/pom.xml b/junction/pom.xml
index 64d65b0..5b341c0 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -60,6 +60,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>joda-time</groupId>
             <artifactId>joda-time</artifactId>
         </dependency>

overdue/pom.xml 5(+5 -0)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index b13bd40..453613f 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -64,6 +64,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

payment/pom.xml 5(+5 -0)

diff --git a/payment/pom.xml b/payment/pom.xml
index 1926973..3013ed7 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -79,6 +79,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
         </dependency>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 44e9514..e2dce78 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -130,6 +130,11 @@
             <artifactId>swagger-jersey-jaxrs</artifactId>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
         </dependency>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index 5c9437f..90b2144 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -43,9 +43,11 @@ import org.killbill.billing.jaxrs.resources.JaxrsResource;
 import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
 import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
 import org.killbill.billing.util.config.definition.RbacConfig;
+import org.killbill.billing.util.config.definition.RedisCacheConfig;
 import org.killbill.billing.util.glue.EhcacheShiroManagerProvider;
 import org.killbill.billing.util.glue.KillBillShiroModule;
 import org.killbill.billing.util.glue.RealmsFromShiroIniProvider;
+import org.killbill.billing.util.glue.RedisShiroManagerProvider;
 import org.killbill.billing.util.glue.SessionDAOProvider;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
@@ -75,8 +77,19 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
 
     @Override
     protected void configureShiroWeb() {
+        final RedisCacheConfig redisCacheConfig = new ConfigurationObjectFactory(new ConfigSource() {
+            @Override
+            public String getString(final String propertyName) {
+                return configSource.getString(propertyName);
+            }
+        }).build(RedisCacheConfig.class);
+
         // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
+        if (redisCacheConfig.isRedisCachingEnabled()) {
+            bind(CacheManager.class).toProvider(RedisShiroManagerProvider.class).asEagerSingleton();
+        } else {
+            bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
+        }
 
         configureShiroForRBAC();
 
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 774613d..2c6008b 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -60,6 +60,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

tenant/pom.xml 11(+11 -0)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index 9c43791..48bf333 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -60,6 +60,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
         </dependency>
@@ -149,6 +154,12 @@
             <artifactId>killbill-queue</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.kill-bill.commons</groupId>
+            <artifactId>killbill-queue</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-all</artifactId>
             <scope>test</scope>
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
index cc48190..9f2ee18 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidation.java
@@ -179,6 +179,7 @@ public class TenantCacheInvalidation {
                         final Collection<CacheInvalidationCallback> callbacks = parent.getCacheInvalidations(tenantKeyAndCookie.getTenantKey());
                         if (!callbacks.isEmpty()) {
                             final InternalTenantContext tenantContext = new InternalTenantContext(cur.getTenantRecordId());
+                            // TODO In case of Redis, we don't want any invalidation, but we still want the events to notify the plugins (ideally, our bus would also support a Topic model)
                             for (final CacheInvalidationCallback callback : callbacks) {
                                 callback.invalidateCache(tenantKeyAndCookie.getTenantKey(), tenantKeyAndCookie.getCookie(), tenantContext);
                             }
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/api/TestDefaultTenant.java b/tenant/src/test/java/org/killbill/billing/tenant/api/TestDefaultTenant.java
new file mode 100644
index 0000000..29a20c7
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/api/TestDefaultTenant.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.api;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.killbill.billing.tenant.TenantTestSuiteNoDB;
+import org.redisson.client.codec.Codec;
+import org.redisson.codec.SerializationCodec;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import io.netty.buffer.ByteBuf;
+
+public class TestDefaultTenant extends TenantTestSuiteNoDB {
+
+    @Test(groups = "fast")
+    public void testExternalizable() throws IOException {
+        final DefaultTenant tenantdata = new DefaultTenant(UUID.randomUUID(), clock.getUTCNow(), clock.getUTCNow(), "er44TT-yy4r", "TTR445ee2", null);
+        final Codec code = new SerializationCodec();
+        final ByteBuf byteBuf = code.getValueEncoder().encode(tenantdata);
+        final DefaultTenant tenantData2 = (DefaultTenant) code.getValueDecoder().decode(byteBuf, null);
+        Assert.assertEquals(tenantData2, tenantdata);
+    }
+}
\ No newline at end of file

usage/pom.xml 5(+5 -0)

diff --git a/usage/pom.xml b/usage/pom.xml
index bf1ace3..94ab0dd 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -55,6 +55,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.inject</groupId>
             <artifactId>javax.inject</artifactId>
             <scope>provided</scope>

util/pom.xml 9(+9 -0)

diff --git a/util/pom.xml b/util/pom.xml
index 7ea6171..5c7126e 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -115,6 +115,11 @@
             <artifactId>metrics-jcache</artifactId>
         </dependency>
         <dependency>
+            <groupId>it.ozimov</groupId>
+            <artifactId>embedded-redis</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>javax.cache</groupId>
             <artifactId>cache-api</artifactId>
         </dependency>
@@ -284,6 +289,10 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.skife.config</groupId>
             <artifactId>config-magic</artifactId>
         </dependency>
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/RedisCacheConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/RedisCacheConfig.java
new file mode 100644
index 0000000..cbe8f34
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/RedisCacheConfig.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.definition;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface RedisCacheConfig extends KillbillConfig {
+
+    @Config("org.killbill.cache.config.redis")
+    @Default("false")
+    @Description("Whether Redis integration for caching is enabled")
+    public boolean isRedisCachingEnabled();
+
+    @Config("org.killbill.cache.config.redis.url")
+    @Default("redis://127.0.0.1:6379")
+    @Description("Redis URL")
+    public String getUrl();
+
+    @Config("org.killbill.cache.config.redis.connectionMinimumIdleSize")
+    @Default("1")
+    @Description("Minimum number of connections")
+    public int getConnectionMinimumIdleSize();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
index 40b55b5..8289c70 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
@@ -42,6 +42,8 @@ import org.killbill.billing.util.cache.TenantOverdueConfigCacheLoader;
 import org.killbill.billing.util.cache.TenantRecordIdCacheLoader;
 import org.killbill.billing.util.cache.TenantStateMachineConfigCacheLoader;
 import org.killbill.billing.util.config.definition.EhCacheConfig;
+import org.killbill.billing.util.config.definition.RedisCacheConfig;
+import org.redisson.api.RedissonClient;
 import org.skife.config.ConfigurationObjectFactory;
 
 import com.google.inject.multibindings.Multibinder;
@@ -50,6 +52,8 @@ import com.google.inject.util.Providers;
 
 public class CacheModule extends KillBillModule {
 
+    public static final String REDIS_CACHE_CLIENT = "redisCacheClient";
+
     public CacheModule(final KillbillConfigSource configSource) {
         super(configSource);
     }
@@ -59,8 +63,16 @@ public class CacheModule extends KillBillModule {
         final EhCacheConfig ehCacheConfig = new ConfigurationObjectFactory(skifeConfigSource).build(EhCacheConfig.class);
         bind(EhCacheConfig.class).toInstance(ehCacheConfig);
 
-        // EhCache specifics
-        bind(CacheManager.class).toProvider(Eh107CacheManagerProvider.class).asEagerSingleton();
+        final RedisCacheConfig redisCacheConfig = new ConfigurationObjectFactory(skifeConfigSource).build(RedisCacheConfig.class);
+        bind(RedisCacheConfig.class).toInstance(redisCacheConfig);
+
+        if (redisCacheConfig.isRedisCachingEnabled()) {
+            bind(RedissonClient.class).annotatedWith(Names.named(REDIS_CACHE_CLIENT)).toProvider(RedissonCacheClientProvider.class).asEagerSingleton();
+            bind(CacheManager.class).toProvider(Redis107CacheManagerProvider.class).asEagerSingleton();
+        } else {
+            bind(RedissonClient.class).annotatedWith(Names.named(REDIS_CACHE_CLIENT)).toProvider(Providers.<RedissonClient>of(null));
+            bind(CacheManager.class).toProvider(Eh107CacheManagerProvider.class).asEagerSingleton();
+        }
 
         // Kill Bill generic cache dispatcher
         bind(CacheControllerDispatcher.class).toProvider(CacheControllerDispatcherProvider.class).asEagerSingleton();
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
index 07a9613..420a565 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -27,6 +27,7 @@ import org.apache.shiro.session.mgt.SessionManager;
 import org.apache.shiro.session.mgt.eis.SessionDAO;
 import org.killbill.billing.platform.api.KillbillConfigSource;
 import org.killbill.billing.util.config.definition.RbacConfig;
+import org.killbill.billing.util.config.definition.RedisCacheConfig;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
@@ -112,8 +113,19 @@ public class KillBillShiroModule extends ShiroModule {
     protected void bindSecurityManager(final AnnotatedBindingBuilder<? super SecurityManager> bind) {
         super.bindSecurityManager(bind);
 
+        final RedisCacheConfig redisCacheConfig = new ConfigurationObjectFactory(new ConfigSource() {
+            @Override
+            public String getString(final String propertyName) {
+                return configSource.getString(propertyName);
+            }
+        }).build(RedisCacheConfig.class);
+
         // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
+        if (redisCacheConfig.isRedisCachingEnabled()) {
+            bind(CacheManager.class).toProvider(RedisShiroManagerProvider.class).asEagerSingleton();
+        } else {
+            bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
+        }
     }
 
     @Override
diff --git a/util/src/main/java/org/killbill/billing/util/glue/Redis107CacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/Redis107CacheManagerProvider.java
new file mode 100644
index 0000000..1d51e13
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/Redis107CacheManagerProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import java.util.Set;
+
+import javax.cache.CacheManager;
+import javax.cache.Caching;
+import javax.cache.spi.CachingProvider;
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.killbill.billing.util.cache.BaseCacheLoader;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.inject.name.Named;
+
+import static org.killbill.billing.util.glue.CacheModule.REDIS_CACHE_CLIENT;
+
+public class Redis107CacheManagerProvider extends RedisCacheProviderBase implements Provider<CacheManager> {
+
+    private static final Logger logger = LoggerFactory.getLogger(Redis107CacheManagerProvider.class);
+
+    private final Set<BaseCacheLoader> cacheLoaders;
+
+    @Inject
+    public Redis107CacheManagerProvider(final MetricRegistry metricRegistry,
+                                        @Named(REDIS_CACHE_CLIENT) final RedissonClient redissonClient,
+                                        final Set<BaseCacheLoader> cacheLoaders) {
+        super(metricRegistry, redissonClient);
+        this.cacheLoaders = cacheLoaders;
+    }
+
+    @Override
+    public CacheManager get() {
+        // JSR-107 registration, required for JMX integration
+        final CachingProvider cachingProvider = Caching.getCachingProvider("org.redisson.jcache.JCachingProvider");
+
+        final CacheManager cacheManager = cachingProvider.getCacheManager();
+
+        for (final BaseCacheLoader<?, ?> cacheLoader : cacheLoaders) {
+            createCache(cacheManager,
+                        cacheLoader.getCacheType().getCacheName(),
+                        cacheLoader.getCacheType().getKeyType(),
+                        cacheLoader.getCacheType().getValueType());
+        }
+
+        return cacheManager;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/RedisCacheProviderBase.java b/util/src/main/java/org/killbill/billing/util/glue/RedisCacheProviderBase.java
new file mode 100644
index 0000000..1a5effc
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/RedisCacheProviderBase.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.cache.CacheManager;
+import javax.cache.configuration.Configuration;
+import javax.cache.configuration.MutableConfiguration;
+
+import org.redisson.api.RedissonClient;
+import org.redisson.jcache.configuration.RedissonConfiguration;
+
+import com.codahale.metrics.MetricRegistry;
+
+abstract class RedisCacheProviderBase extends CacheProviderBase {
+
+    private final RedissonClient redissonClient;
+
+    RedisCacheProviderBase(final MetricRegistry metricRegistry, final RedissonClient redissonClient) {
+        super(metricRegistry);
+        this.redissonClient = redissonClient;
+    }
+
+    <K, V> void createCache(final CacheManager cacheManager, final String cacheName, final Class<K> keyType, final Class<V> valueType) {
+        final Configuration jcacheConfig = new MutableConfiguration().setTypes(keyType, valueType);
+
+        final Configuration redissonConfiguration = RedissonConfiguration.fromInstance(redissonClient, jcacheConfig);
+
+        createCache(cacheManager, cacheName, redissonConfiguration);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManager.java b/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManager.java
new file mode 100644
index 0000000..0a08407
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManager.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+import javax.cache.Cache.Entry;
+
+import org.apache.shiro.cache.Cache;
+import org.apache.shiro.cache.CacheException;
+import org.apache.shiro.cache.CacheManager;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+
+public class RedisShiroManager extends RedisCacheProviderBase implements CacheManager {
+
+    private static final Logger log = LoggerFactory.getLogger(RedisShiroManager.class);
+
+    private final javax.cache.CacheManager eh107CacheManager;
+
+    public RedisShiroManager(final javax.cache.CacheManager eh107CacheManager,
+                             final MetricRegistry metricRegistry,
+                             final RedissonClient redissonClient) {
+        super(metricRegistry, redissonClient);
+        this.eh107CacheManager = eh107CacheManager;
+    }
+
+    @Override
+    public <K, V> Cache<K, V> getCache(final String name) throws CacheException {
+        log.trace("Acquiring RedisShiro instance named [{}]", name);
+
+        javax.cache.Cache<Object, Object> cache = eh107CacheManager.getCache(name, Object.class, Object.class);
+
+        if (cache == null) {
+            log.info("Cache with name {} does not yet exist.  Creating now.", name);
+            createCache(eh107CacheManager, name, Object.class, Object.class);
+            cache = eh107CacheManager.getCache(name, Object.class, Object.class);
+            log.info("Added RedisShiro named [{}]", name);
+        } else {
+            log.info("Using existing RedisShiro named [{}]", name);
+        }
+
+        return new RedisCache<K, V>(cache);
+    }
+
+    private static final class RedisCache<K, V> implements Cache<K, V> {
+
+        private final javax.cache.Cache<K, V> cache;
+
+        public RedisCache(final javax.cache.Cache cache) {
+            this.cache = cache;
+
+        }
+
+        @Override
+        public V get(final K key) throws CacheException {
+            return cache.get(key);
+        }
+
+        @Override
+        public V put(final K key, final V value) throws CacheException {
+            V previousValue;
+            while (true) {
+                previousValue = cache.get(key);
+                if (previousValue == null) {
+                    if (cache.putIfAbsent(key, value)) {
+                        break;
+                    }
+                } else {
+                    if (cache.replace(key, value)) {
+                        break;
+                    }
+                }
+            }
+
+            return previousValue;
+        }
+
+        @Override
+        public V remove(final K key) throws CacheException {
+            V previousValue;
+            while (true) {
+                previousValue = cache.get(key);
+                if (previousValue == null) {
+                    break;
+                } else {
+                    if (cache.remove(key)) {
+                        break;
+                    }
+                }
+            }
+
+            return previousValue;
+        }
+
+        @Override
+        public void clear() throws CacheException {
+            cache.clear();
+        }
+
+        @Override
+        public int size() {
+            return keys().size();
+        }
+
+        @Override
+        public Set<K> keys() {
+            final Set<K> result = new HashSet<K>();
+            for (final Entry<K, V> entry : cache) {
+                result.add(entry.getKey());
+            }
+            return result;
+        }
+
+        @Override
+        public Collection<V> values() {
+            final Collection<V> result = new LinkedList<V>();
+            for (final Entry<K, V> entry : cache) {
+                result.add(entry.getValue());
+            }
+            return result;
+        }
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManagerProvider.java
new file mode 100644
index 0000000..bcfa133
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/RedisShiroManagerProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.cache.CacheManager;
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.mgt.SecurityManager;
+import org.redisson.api.RedissonClient;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.inject.name.Named;
+
+import static org.killbill.billing.util.glue.CacheModule.REDIS_CACHE_CLIENT;
+
+public class RedisShiroManagerProvider implements Provider<RedisShiroManager> {
+
+    private final CacheManager eh107CacheManager;
+    private final SecurityManager securityManager;
+    private final MetricRegistry metricRegistry;
+    private final RedissonClient redissonClient;
+
+    @Inject
+    public RedisShiroManagerProvider(final SecurityManager securityManager,
+                                     final CacheManager eh107CacheManager,
+                                     final MetricRegistry metricRegistry,
+                                     @Named(REDIS_CACHE_CLIENT) final RedissonClient redissonClient) {
+        this.securityManager = securityManager;
+        this.eh107CacheManager = eh107CacheManager;
+        this.metricRegistry = metricRegistry;
+        this.redissonClient = redissonClient;
+    }
+
+    @Override
+    public RedisShiroManager get() {
+        // Same Redis manager instance as the rest of the system
+        final RedisShiroManager shiroRedisManager = new RedisShiroManager(eh107CacheManager, metricRegistry, redissonClient);
+
+        if (securityManager instanceof DefaultSecurityManager) {
+            // For RBAC only (see also KillbillJdbcTenantRealmProvider)
+            final DefaultSecurityManager securityManager = (DefaultSecurityManager) this.securityManager;
+            securityManager.setCacheManager(shiroRedisManager);
+            securityManager.setSubjectDAO(new KillBillSubjectDAO());
+        }
+
+        return shiroRedisManager;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/RedissonCacheClientProvider.java b/util/src/main/java/org/killbill/billing/util/glue/RedissonCacheClientProvider.java
new file mode 100644
index 0000000..8d4f958
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/RedissonCacheClientProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.util.config.definition.RedisCacheConfig;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.client.codec.Codec;
+import org.redisson.codec.SerializationCodec;
+import org.redisson.config.Config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Provider;
+
+public class RedissonCacheClientProvider implements Provider<RedissonClient> {
+
+    private final String address;
+    private final int connectionMinimumIdleSize;
+
+    @Inject
+    public RedissonCacheClientProvider(final RedisCacheConfig cacheConfig) {
+        this(cacheConfig.getUrl(), cacheConfig.getConnectionMinimumIdleSize());
+    }
+
+    @VisibleForTesting
+    public RedissonCacheClientProvider(final String address, final int connectionMinimumIdleSize) {
+        this.address = address;
+        this.connectionMinimumIdleSize = connectionMinimumIdleSize;
+    }
+
+    @Override
+    public RedissonClient get() {
+        // JDK serialization codec for now, but we can do better in speed and space
+        final Codec codec = new SerializationCodec();
+
+        final Config redissonCfg = new Config();
+        redissonCfg.setCodec(codec)
+                   .useSingleServer()
+                   .setAddress(address)
+                   .setConnectionMinimumIdleSize(connectionMinimumIdleSize);
+        return Redisson.create(redissonCfg);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/SessionDAOProvider.java b/util/src/main/java/org/killbill/billing/util/glue/SessionDAOProvider.java
index a771c9c..0cfba7e 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/SessionDAOProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/SessionDAOProvider.java
@@ -27,7 +27,9 @@ import org.apache.shiro.session.mgt.SessionManager;
 import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
 import org.apache.shiro.session.mgt.eis.SessionDAO;
 import org.killbill.billing.util.config.definition.RbacConfig;
+import org.killbill.billing.util.config.definition.RedisCacheConfig;
 import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
+import org.killbill.billing.util.security.shiro.dao.RedisSessionDao;
 import org.skife.jdbi.v2.IDBI;
 
 import static org.killbill.billing.util.glue.IDBISetup.MAIN_RO_IDBI_NAMED;
@@ -38,18 +40,25 @@ public class SessionDAOProvider implements Provider<SessionDAO> {
     private final IDBI dbi;
     private final IDBI roDbi;
     private final RbacConfig rbacConfig;
+    private final RedisCacheConfig redisCacheConfig;
 
     @Inject
-    public SessionDAOProvider(final IDBI dbi, @Named(MAIN_RO_IDBI_NAMED) final IDBI roDbi, final SessionManager sessionManager, final RbacConfig rbacConfig) {
+    public SessionDAOProvider(final IDBI dbi, @Named(MAIN_RO_IDBI_NAMED) final IDBI roDbi, final SessionManager sessionManager, final RbacConfig rbacConfig, final RedisCacheConfig redisCacheConfig) {
         this.sessionManager = sessionManager;
         this.dbi = dbi;
         this.roDbi = roDbi;
         this.rbacConfig = rbacConfig;
+        this.redisCacheConfig = redisCacheConfig;
     }
 
     @Override
     public SessionDAO get() {
-        final CachingSessionDAO sessionDao = new JDBCSessionDao(dbi, roDbi);
+        final CachingSessionDAO sessionDao;
+        if (redisCacheConfig.isRedisCachingEnabled()) {
+            sessionDao = new RedisSessionDao();
+        } else {
+            sessionDao = new JDBCSessionDao(dbi, roDbi);
+        }
 
         if (sessionManager instanceof DefaultSessionManager) {
             final DefaultSessionManager defaultSessionManager = (DefaultSessionManager) sessionManager;
diff --git a/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
index 24dbea6..c181efc 100644
--- a/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
+++ b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
@@ -25,7 +25,7 @@ import com.fasterxml.jackson.datatype.joda.JodaModule;
 
 public class ObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
 
-    public ObjectMapper(final SmileFactory f) {
+    private ObjectMapper(final SmileFactory f) {
         super(f);
         this.registerModule(new JodaModule());
         this.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@@ -35,4 +35,9 @@ public class ObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
     public ObjectMapper() {
         this(null);
     }
+
+    @Override
+    public com.fasterxml.jackson.databind.ObjectMapper copy() {
+        return new ObjectMapper();
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RedisSessionDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RedisSessionDao.java
new file mode 100644
index 0000000..4d356d4
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RedisSessionDao.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
+import org.killbill.billing.util.UUIDs;
+
+public class RedisSessionDao extends CachingSessionDAO {
+
+    @Override
+    protected Serializable doCreate(final Session session) {
+        final UUID sessionId = UUIDs.randomUUID();
+        // See SessionModelDao#toSimpleSession for why we use toString()
+        final String sessionIdAsString = sessionId.toString();
+        assignSessionId(session, sessionIdAsString);
+        // Make sure to return a String here as well, or Shiro will cache the Session with a UUID key
+        // while it is expecting String
+        return sessionIdAsString;
+    }
+
+    protected Session doReadSession(final Serializable sessionId) {
+        // Should never be executed
+        throw new IllegalStateException("Session should be in Redis");
+    }
+
+    protected void doUpdate(final Session session) {
+        // Does nothing - parent class persists to cache.
+    }
+
+    @Override
+    protected void doDelete(final Session session) {
+        // Does nothing - parent class removes from cache.
+    }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java
index 68b9717..1f505ab 100644
--- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java
@@ -34,11 +34,14 @@ import org.killbill.billing.callcontext.MutableInternalCallContext;
 import org.killbill.billing.platform.api.KillbillConfigSource;
 import org.killbill.billing.platform.test.config.TestKillbillConfigSource;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.glue.RedissonCacheClientProvider;
 import org.killbill.clock.Clock;
 import org.killbill.clock.ClockMock;
+import org.killbill.clock.DistributedClockMock;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
+import org.redisson.api.RedissonClient;
 import org.skife.config.ConfigSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,6 +58,7 @@ import org.testng.annotations.Listeners;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
+import redis.embedded.RedisServer;
 
 import static org.testng.ITestResult.CREATED;
 import static org.testng.ITestResult.FAILURE;
@@ -79,6 +83,8 @@ public class GuicyKillbillTestSuite implements IHookable {
     protected KillbillConfigSource configSource;
     protected ConfigSource skifeConfigSource;
 
+    private RedissonClient redissonClient;
+
     @Inject
     protected InternalCallContextFactory internalCallContextFactory;
 
@@ -88,6 +94,7 @@ public class GuicyKillbillTestSuite implements IHookable {
     @Inject
     protected MutableCallContext callContext;
 
+    private RedisServer redisServer;
 
     private boolean hasFailed = false;
 
@@ -212,9 +219,25 @@ public class GuicyKillbillTestSuite implements IHookable {
 
     @BeforeSuite(alwaysRun = true)
     public void globalBeforeSuite() {
-        theRealClock.resetDeltaFromReality();
+        if (Boolean.valueOf(System.getProperty("killbill.test.redis", "false"))) {
+            redisServer = new RedisServer(56379);
+            redisServer.start();
+
+            redissonClient = new RedissonCacheClientProvider("redis://127.0.0.1:56379", 1).get();
+
+            theRealClock = new DistributedClockMock();
+            ((DistributedClockMock) theRealClock).setRedissonClient(redissonClient);
+
+            extraPropertiesForTestSuite = ImmutableMap.<String, String>of("org.killbill.cache.config.redis", "true",
+                                                                          "org.killbill.cache.config.redis.url", "redis://127.0.0.1:56379");
+        } else {
+            theRealClock.resetDeltaFromReality();
 
-        extraPropertiesForTestSuite = ImmutableMap.<String, String>of();
+            extraPropertiesForTestSuite = ImmutableMap.<String, String>of();
+        }
+
+        // The clock needs to be setup early in @BeforeSuite, as it is needed when starting the server, but see below
+        clock = theRealClock;
     }
 
     @BeforeClass(alwaysRun = true)
@@ -250,6 +273,12 @@ public class GuicyKillbillTestSuite implements IHookable {
 
     @AfterSuite(alwaysRun = true)
     public void globalAfterSuite() {
+        if (redissonClient != null) {
+            redissonClient.shutdown();
+        }
+        if (redisServer != null) {
+            redisServer.stop();
+        }
     }
 
     public boolean hasFailed() {
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
index c282bd2..72bc9a4 100644
--- a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
@@ -39,6 +39,7 @@ import javax.sql.DataSource;
 
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.redisson.api.RedissonClient;
 import org.skife.jdbi.v2.IDBI;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -50,6 +51,7 @@ import org.testng.annotations.BeforeSuite;
 import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
+import static org.killbill.billing.util.glue.CacheModule.REDIS_CACHE_CLIENT;
 import static org.killbill.billing.util.glue.IDBISetup.MAIN_RO_IDBI_NAMED;
 
 public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite {
@@ -76,6 +78,11 @@ public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
     @Inject(optional = true)
     protected CacheManager cacheManager;
 
+    @Nullable
+    @Inject(optional = true)
+    @Named(REDIS_CACHE_CLIENT)
+    protected RedissonClient redissonCachingClient;
+
     @BeforeSuite(groups = "slow")
     public void beforeSuite() throws Exception {
         // Hack to configure log4jdbc -- properties used by tests will be properly setup in @BeforeClass
@@ -118,6 +125,10 @@ public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
             cacheManager.close();
         }
 
+        if (redissonCachingClient != null) {
+            redissonCachingClient.shutdown();
+        }
+
         try {
             DBTestingHelper.get().getInstance().stop();
         } catch (final Exception ignored) {