diff --git a/pom.xml b/pom.xml index c9fd15d2cb..e6ff5bc70b 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,27 @@ 2.10.1 + + + + com.google.guava + guava + 33.0.0-jre + true + + + com.github.ben-manes.caffeine + caffeine + 2.9.3 + true + + + net.openhft + zero-allocation-hashing + 0.16 + true + + com.kohlschutter.junixsocket @@ -90,6 +111,7 @@ 1.19.0 test + junit diff --git a/src/main/java/redis/clients/jedis/ClientSideCache.java b/src/main/java/redis/clients/jedis/ClientSideCache.java index 62c5be28c2..c2c9248acf 100644 --- a/src/main/java/redis/clients/jedis/ClientSideCache.java +++ b/src/main/java/redis/clients/jedis/ClientSideCache.java @@ -1,71 +1,104 @@ package redis.clients.jedis; import java.nio.ByteBuffer; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; - -import redis.clients.jedis.exceptions.JedisException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import redis.clients.jedis.util.SafeEncoder; -public class ClientSideCache { +/** + * The class to manage the client-side caching. User can provide any of implementation of this class to the client + * object; e.g. {@link redis.clients.jedis.util.CaffeineCSC CaffeineCSC} or + * {@link redis.clients.jedis.util.GuavaCSC GuavaCSC} or a custom implementation of their own. + */ +public abstract class ClientSideCache { - private final Map cache; + protected static final int DEFAULT_MAXIMUM_SIZE = 10_000; + protected static final int DEFAULT_EXPIRE_SECONDS = 100; - public ClientSideCache() { - this.cache = new HashMap<>(); - } + private final Map> keyToCommandHashes; - /** - * For testing purpose only. - * @param map - */ - ClientSideCache(Map map) { - this.cache = map; + protected ClientSideCache() { + this.keyToCommandHashes = new ConcurrentHashMap<>(); } + protected abstract void invalidateAllCommandHashes(); + + protected abstract void invalidateCommandHashes(Iterable hashes); + + protected abstract void put(long hash, Object value); + + protected abstract Object get(long hash); + + protected abstract long getCommandHash(CommandObject command); + public final void clear() { - cache.clear(); + invalidateAllKeysAndCommandHashes(); } - public final void invalidateKeys(List list) { + final void invalidate(List list) { if (list == null) { - clear(); + invalidateAllKeysAndCommandHashes(); return; } - list.forEach(this::invalidateKey); + list.forEach(this::invalidateKeyAndRespectiveCommandHashes); } - private void invalidateKey(Object key) { - if (key instanceof byte[]) { - cache.remove(convertKey((byte[]) key)); - } else { - throw new JedisException("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); - } + private void invalidateAllKeysAndCommandHashes() { + invalidateAllCommandHashes(); + keyToCommandHashes.clear(); } - protected void setKey(Object key, Object value) { - cache.put(getMapKey(key), value); - } + private void invalidateKeyAndRespectiveCommandHashes(Object key) { + if (!(key instanceof byte[])) { + throw new AssertionError("" + key.getClass().getSimpleName() + " is not supported. Value: " + String.valueOf(key)); + } - protected T getValue(Object key) { - return (T) getMapValue(key); - } + final ByteBuffer mapKey = makeKeyForKeyToCommandHashes((byte[]) key); - private Object getMapValue(Object key) { - return cache.get(getMapKey(key)); + Set hashes = keyToCommandHashes.get(mapKey); + if (hashes != null) { + invalidateCommandHashes(hashes); + keyToCommandHashes.remove(mapKey); + } } - private ByteBuffer getMapKey(Object key) { - if (key instanceof byte[]) { - return convertKey((byte[]) key); - } else { - return convertKey(SafeEncoder.encode(String.valueOf(key))); + final T getValue(Function, T> loader, CommandObject command, String... keys) { + + final long hash = getCommandHash(command); + + T value = (T) get(hash); + if (value != null) { + return value; } + + value = loader.apply(command); + if (value != null) { + put(hash, value); + for (String key : keys) { + ByteBuffer mapKey = makeKeyForKeyToCommandHashes(key); + if (keyToCommandHashes.containsKey(mapKey)) { + keyToCommandHashes.get(mapKey).add(hash); + } else { + Set set = new HashSet<>(); + set.add(hash); + keyToCommandHashes.put(mapKey, set); + } + } + } + + return value; + } + + private ByteBuffer makeKeyForKeyToCommandHashes(String key) { + return makeKeyForKeyToCommandHashes(SafeEncoder.encode(key)); } - private static ByteBuffer convertKey(byte[] b) { + private static ByteBuffer makeKeyForKeyToCommandHashes(byte[] b) { return ByteBuffer.wrap(b); } } diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index 4af1261cd4..4bd82fec1e 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -248,7 +248,7 @@ private static void processPush(final RedisInputStream is, ClientSideCache cache //System.out.println("PUSH: " + SafeEncoder.encodeObject(list)); if (list.size() == 2 && list.get(0) instanceof byte[] && Arrays.equals(INVALIDATE_BYTES, (byte[]) list.get(0))) { - cache.invalidateKeys((List) list.get(1)); + cache.invalidate((List) list.get(1)); } } diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index ba7b36b134..3a2dec9d77 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -295,6 +295,14 @@ public void setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig co this.commandObjects.setBroadcastAndRoundRobinConfig(this.broadcastAndRoundRobinConfig); } + private T executeClientSideCacheCommand(CommandObject command, String... keys) { + if (clientSideCache == null) { + return executeCommand(command); + } + + return clientSideCache.getValue((cmd) -> executeCommand(cmd), command, keys); + } + public String ping() { return checkAndBroadcastCommand(commandObjects.ping()); } @@ -749,15 +757,7 @@ public String set(String key, String value, SetParams params) { @Override public String get(String key) { - if (clientSideCache != null) { - String cachedValue = clientSideCache.getValue(key); - if (cachedValue != null) return cachedValue; - - String value = executeCommand(commandObjects.get(key)); - if (value != null) clientSideCache.setKey(key, value); - return value; - } - return executeCommand(commandObjects.get(key)); + return executeClientSideCacheCommand(commandObjects.get(key), key); } @Override diff --git a/src/main/java/redis/clients/jedis/util/CaffeineCSC.java b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java new file mode 100644 index 0000000000..3bce3504b3 --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/CaffeineCSC.java @@ -0,0 +1,93 @@ +package redis.clients.jedis.util; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import net.openhft.hashing.LongHashFunction; +import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.args.Rawable; + +public class CaffeineCSC extends ClientSideCache { + + private static final LongHashFunction DEFAULT_HASH_FUNCTION = LongHashFunction.xx3(); + + private final Cache cache; + private final LongHashFunction function; + + public CaffeineCSC(Cache caffeineCache, LongHashFunction hashFunction) { + this.cache = caffeineCache; + this.function = hashFunction; + } + + @Override + protected final void invalidateAllCommandHashes() { + cache.invalidateAll(); + } + + @Override + protected void invalidateCommandHashes(Iterable hashes) { + cache.invalidateAll(hashes); + } + + @Override + protected void put(long hash, Object value) { + cache.put(hash, value); + } + + @Override + protected Object get(long hash) { + return cache.getIfPresent(hash); + } + + @Override + protected final long getCommandHash(CommandObject command) { + long[] nums = new long[command.getArguments().size() + 1]; + int idx = 0; + for (Rawable raw : command.getArguments()) { + nums[idx++] = function.hashBytes(raw.getRaw()); + } + nums[idx] = function.hashInt(command.getBuilder().hashCode()); + return function.hashLongs(nums); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long maximumSize = DEFAULT_MAXIMUM_SIZE; + private long expireTime = DEFAULT_EXPIRE_SECONDS; + private final TimeUnit expireTimeUnit = TimeUnit.SECONDS; + + private LongHashFunction hashFunction = DEFAULT_HASH_FUNCTION; + + private Builder() { } + + public Builder maximumSize(int size) { + this.maximumSize = size; + return this; + } + + public Builder ttl(int seconds) { + this.expireTime = seconds; + return this; + } + + public Builder hashFunction(LongHashFunction function) { + this.hashFunction = function; + return this; + } + + public CaffeineCSC build() { + Caffeine cb = Caffeine.newBuilder(); + + cb.maximumSize(maximumSize); + + cb.expireAfterWrite(expireTime, expireTimeUnit); + + return new CaffeineCSC(cb.build(), hashFunction); + } + } +} diff --git a/src/main/java/redis/clients/jedis/util/GuavaCSC.java b/src/main/java/redis/clients/jedis/util/GuavaCSC.java new file mode 100644 index 0000000000..d9973b7dc6 --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/GuavaCSC.java @@ -0,0 +1,90 @@ +package redis.clients.jedis.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hasher; +import java.util.concurrent.TimeUnit; +import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; + +public class GuavaCSC extends ClientSideCache { + + private static final HashFunction DEFAULT_HASH_FUNCTION = com.google.common.hash.Hashing.fingerprint2011(); + + private final Cache cache; + private final HashFunction function; + + public GuavaCSC(Cache guavaCache, HashFunction hashFunction) { + this.cache = guavaCache; + this.function = hashFunction; + } + + @Override + protected final void invalidateAllCommandHashes() { + cache.invalidateAll(); + } + + @Override + protected void invalidateCommandHashes(Iterable hashes) { + cache.invalidateAll(hashes); + } + + @Override + protected void put(long hash, Object value) { + cache.put(hash, value); + } + + @Override + protected Object get(long hash) { + return cache.getIfPresent(hash); + } + + @Override + protected final long getCommandHash(CommandObject command) { + Hasher hasher = function.newHasher(); + command.getArguments().forEach(raw -> hasher.putBytes(raw.getRaw())); + hasher.putInt(command.getBuilder().hashCode()); + return hasher.hash().asLong(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long maximumSize = DEFAULT_MAXIMUM_SIZE; + private long expireTime = DEFAULT_EXPIRE_SECONDS; + private final TimeUnit expireTimeUnit = TimeUnit.SECONDS; + + private HashFunction hashFunction = DEFAULT_HASH_FUNCTION; + + private Builder() { } + + public Builder maximumSize(int size) { + this.maximumSize = size; + return this; + } + + public Builder ttl(int seconds) { + this.expireTime = seconds; + return this; + } + + public Builder hashFunction(HashFunction function) { + this.hashFunction = function; + return this; + } + + public GuavaCSC build() { + CacheBuilder cb = CacheBuilder.newBuilder(); + + cb.maximumSize(maximumSize); + + cb.expireAfterWrite(expireTime, expireTimeUnit); + + return new GuavaCSC(cb.build(), hashFunction); + } + } +} diff --git a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java index 3c8bc18c5c..60ddf002d7 100644 --- a/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisClusterClientSideCacheTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Set; @@ -14,6 +13,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.hamcrest.Matchers; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisClusterClientSideCacheTest extends JedisClusterTestBase { @@ -31,7 +31,7 @@ public class JedisClusterClientSideCacheTest extends JedisClusterTestBase { @Test public void simple() { - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC())) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.del("foo"); @@ -41,8 +41,8 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + HashMap map = new HashMap<>(); + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -60,7 +60,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC())) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.flushAll(); @@ -70,8 +70,8 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + HashMap map = new HashMap<>(); + try (JedisCluster jedis = new JedisCluster(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java index ad4313a4b7..2e641e0f3a 100644 --- a/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisPooledClientSideCacheTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.HashMap; import java.util.function.Supplier; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -12,6 +11,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisPooledClientSideCacheTest { @@ -42,7 +42,7 @@ public void tearDown() throws Exception { @Test public void simple() { - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC())) { control.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); control.del("foo"); @@ -52,8 +52,8 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + HashMap map = new HashMap<>(); + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -71,7 +71,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache())) { + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC())) { control.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); control.flushAll(); @@ -81,8 +81,8 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new ClientSideCache(map), singleConnectionPoolConfig.get())) { + HashMap map = new HashMap<>(); + try (JedisPooled jedis = new JedisPooled(hnp, clientConfig.get(), new MapCSC(map), singleConnectionPoolConfig.get())) { control.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java index 9af243ffc7..9e5f720933 100644 --- a/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/JedisSentineledClientSideCacheTest.java @@ -4,16 +4,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.hamcrest.Matchers; import org.junit.Test; +import redis.clients.jedis.util.MapCSC; public class JedisSentineledClientSideCacheTest { @@ -28,16 +26,9 @@ public class JedisSentineledClientSideCacheTest { private static final JedisClientConfig sentinelClientConfig = DefaultJedisClientConfig.builder().resp3().build(); - private static final Supplier> singleConnectionPoolConfig - = () -> { - ConnectionPoolConfig poolConfig = new ConnectionPoolConfig(); - poolConfig.setMaxTotal(1); - return poolConfig; - }; - @Test public void simple() { - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.del("foo"); @@ -47,8 +38,8 @@ public void simple() { @Test public void simpleWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(map), sentinels, sentinelClientConfig)) { + HashMap map = new HashMap<>(); + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); @@ -66,7 +57,7 @@ public void simpleWithSimpleMap() { @Test public void flushAll() { - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(), sentinels, sentinelClientConfig)) { + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertEquals("bar", jedis.get("foo")); jedis.flushAll(); @@ -76,8 +67,8 @@ public void flushAll() { @Test public void flushAllWithSimpleMap() { - HashMap map = new HashMap<>(); - try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new ClientSideCache(map), sentinels, sentinelClientConfig)) { + HashMap map = new HashMap<>(); + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, masterClientConfig, new MapCSC(map), sentinels, sentinelClientConfig)) { jedis.set("foo", "bar"); assertThat(map, Matchers.aMapWithSize(0)); assertEquals("bar", jedis.get("foo")); diff --git a/src/test/java/redis/clients/jedis/util/MapCSC.java b/src/test/java/redis/clients/jedis/util/MapCSC.java new file mode 100644 index 0000000000..eb229036ea --- /dev/null +++ b/src/test/java/redis/clients/jedis/util/MapCSC.java @@ -0,0 +1,50 @@ +package redis.clients.jedis.util; + +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import redis.clients.jedis.ClientSideCache; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.args.Rawable; + +public class MapCSC extends ClientSideCache { + + private final Map cache; + + public MapCSC() { + this(new ConcurrentHashMap<>()); + } + + public MapCSC(Map map) { + this.cache = map; + } + + @Override + protected final void invalidateAllCommandHashes() { + cache.clear(); + } + + @Override + protected void invalidateCommandHashes(Iterable hashes) { + hashes.forEach(hash -> cache.remove(hash)); + } + + @Override + protected void put(long hash, Object value) { + cache.put(hash, value); + } + + @Override + protected Object get(long hash) { + return cache.get(hash); + } + + @Override + protected final long getCommandHash(CommandObject command) { + long result = 1; + for (Rawable raw : command.getArguments()) { + result = 31 * result + Arrays.hashCode(raw.getRaw()); + } + return 31 * result + command.getBuilder().hashCode(); + } +}