diff --git a/distribution/pom.xml b/distribution/pom.xml
index 04e205158d1b..f9466bc6b656 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -95,6 +95,8 @@
io.druid.extensions:druid-s3-extensions
-c
io.druid.extensions:druid-cloudfiles-extensions
+ -c
+ io.druid.extensions:druid-redis-cache
diff --git a/docs/content/configuration/index.md b/docs/content/configuration/index.md
index 7a259870993e..ef9819f81dab 100644
--- a/docs/content/configuration/index.md
+++ b/docs/content/configuration/index.md
@@ -232,7 +232,7 @@ You can enable caching of results at the broker, historical, or realtime level u
|Property|Description|Default|
|--------|-----------|-------|
-|`druid.cache.type`|`local`, `memcached`|The type of cache to use for queries.|`local`|
+|`druid.cache.type`|`local`, `memcached`, `redis`|The type of cache to use for queries.|`local`|
|`druid.(broker|historical|realtime).cache.unCacheable`|All druid query types|All query types to not cache.|["groupBy", "select"]|
|`druid.(broker|historical|realtime).cache.useCache`|Whether to use cache for getting query results.|false|
|`druid.(broker|historical|realtime).cache.populateCache`|Whether to populate cache.|false|
@@ -255,6 +255,16 @@ You can enable caching of results at the broker, historical, or realtime level u
|`druid.cache.maxObjectSize`|Maximum object size in bytes for a Memcached object.|52428800 (50 MB)|
|`druid.cache.memcachedPrefix`|Key prefix for all keys in Memcached.|druid|
+#### Redis
+
+|Property|Description|Default|
+|--------|-----------|-------|
+|`druid.cache.hosts`|Redis hosts|["127.0.0.1:6379"]|
+|`druid.cache.timeout`|Connection / operations timeout|30000|
+|`druid.cache.prefix`|Key prefix|druid|
+|`druid.cache.expiration`|Redis key expiration time|-1 (no expiration)|
+|`druid.cache.poolSize`|Size of Redis connections pool|1|
+
### Indexing Service Discovery
This config is used to find the [Indexing Service](../design/indexing-service.html) using Curator service discovery. Only required if you are actually running an indexing service.
diff --git a/extensions/redis-cache/pom.xml b/extensions/redis-cache/pom.xml
new file mode 100644
index 000000000000..471af4b68967
--- /dev/null
+++ b/extensions/redis-cache/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+
+ 4.0.0
+
+ io.druid.extensions
+ druid-redis-cache
+ druid-redis-cache
+ druid-redis-cache
+
+
+ io.druid
+ druid
+ 0.9.0-SNAPSHOT
+ ../../pom.xml
+
+
+
+
+ io.druid
+ druid-api
+ provided
+
+
+ io.druid
+ druid-server
+ ${project.parent.version}
+ provided
+
+
+ redis.clients
+ jedis
+ 2.7.2
+ jar
+ compile
+
+
+ net.jpountz.lz4
+ lz4
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ provided
+
+
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/extensions/redis-cache/src/main/java/io/druid/client/cache/RedisCache.java b/extensions/redis-cache/src/main/java/io/druid/client/cache/RedisCache.java
new file mode 100644
index 000000000000..585e2591970c
--- /dev/null
+++ b/extensions/redis-cache/src/main/java/io/druid/client/cache/RedisCache.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.client.cache;
+
+import com.google.common.base.Function;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.metamx.common.logger.Logger;
+import com.metamx.emitter.service.ServiceEmitter;
+import io.druid.collections.LoadBalancingPool;
+import io.druid.collections.ResourceHolder;
+import io.druid.collections.StupidResourceHolder;
+import net.jpountz.lz4.LZ4Exception;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.ShardedJedis;
+import redis.clients.jedis.ShardedJedisPipeline;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.exceptions.JedisException;
+import redis.clients.util.Hashing;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class RedisCache implements Cache
+{
+ private static final Logger log = new Logger(RedisCache.class);
+
+ private final Supplier> supplier;
+ private final String prefix;
+ private final long expiration;
+ private final RedisTranscoder transcoder;
+ private final AtomicLong hitCount = new AtomicLong(0);
+ private final AtomicLong missCount = new AtomicLong(0);
+ private final AtomicLong timeoutCount = new AtomicLong(0);
+ private final AtomicLong errorCount = new AtomicLong(0);
+
+ public static Cache create(final RedisCacheConfig config)
+ {
+ final Supplier> clientSupplier;
+
+ if (config.getPoolSize() > 1) {
+ clientSupplier = new LoadBalancingPool<>(
+ config.getPoolSize(),
+ new Supplier()
+ {
+ @Override
+ public ShardedJedis get()
+ {
+ return new ShardedJedis(config.getShardsInfo(), Hashing.MURMUR_HASH);
+ }
+ }
+ );
+ } else {
+ clientSupplier = Suppliers.>ofInstance(
+ StupidResourceHolder.create(new ShardedJedis(config.getShardsInfo(), Hashing.MURMUR_HASH))
+ );
+ }
+
+ return new RedisCache(clientSupplier, config);
+ }
+
+ public RedisCache(Supplier> supplier, RedisCacheConfig config)
+ {
+ this.supplier = supplier;
+ prefix = config.getPrefix();
+ expiration = config.getExpiration();
+ transcoder = new RedisTranscoder();
+ }
+
+ @Override
+ public byte[] get(NamedKey key)
+ {
+ byte[] result = null;
+
+ try (ResourceHolder clientHolder = supplier.get()) {
+ final ShardedJedis jedis = clientHolder.get();
+ final byte[] itemKey = CacheImplUtils.computeKeyHash(prefix, key).getBytes();
+ final byte[] serialized = jedis.get(itemKey);
+
+ if (serialized != null) {
+ result = deserialize(key, serialized);
+ hitCount.incrementAndGet();
+ }
+ else {
+ missCount.incrementAndGet();
+ }
+ }
+ catch (IOException | JedisException | LZ4Exception ex) {
+ this.countException(ex);
+ log.warn(ex, "Error getting cache item");
+ }
+
+ return result;
+ }
+
+ @Override
+ public void put(NamedKey key, byte[] value)
+ {
+ final byte[] serialized = serialize(key, value);
+
+ try (ResourceHolder clientHolder = supplier.get()) {
+ final ShardedJedis jedis = clientHolder.get();
+
+ final byte[] itemKey = CacheImplUtils.computeKeyHash(prefix, key).getBytes();
+ Jedis shard = jedis.getShard(itemKey);
+
+ if (expiration > 0) {
+ shard.psetex(itemKey, expiration, serialized);
+ }
+ else {
+ shard.set(itemKey, serialized);
+ }
+
+ final byte[] setKey = CacheImplUtils.computeNamespaceHash(prefix, key.namespace).getBytes();
+ shard = jedis.getShard(setKey);
+ shard.sadd(setKey, key.key);
+ }
+ catch (IOException | JedisException ex) {
+ this.countException(ex);
+ log.warn(ex, "Error putting cache item");
+ }
+ }
+
+ @Override
+ public Map getBulk(Iterable keys)
+ {
+ final Map keyLookup = Maps.uniqueIndex(
+ keys,
+ new Function()
+ {
+ @Override
+ public byte[] apply(
+ @Nullable NamedKey input
+ )
+ {
+ return CacheImplUtils.computeKeyHash(prefix, input).getBytes();
+ }
+ }
+ );
+
+ // Sometimes broker passes empty keys list to getBulk()
+ if (keyLookup.size() == 0) {
+ return ImmutableMap.of();
+ }
+
+ final Map results = Maps.newHashMap();
+ final byte[][] keysArray = Iterables.toArray(keyLookup.keySet(), byte[].class);
+ final List