diff --git a/redisson/src/main/java/org/redisson/RedissonMapCache.java b/redisson/src/main/java/org/redisson/RedissonMapCache.java index 6c446b398..546082ce8 100644 --- a/redisson/src/main/java/org/redisson/RedissonMapCache.java +++ b/redisson/src/main/java/org/redisson/RedissonMapCache.java @@ -82,6 +82,7 @@ public class RedissonMapCache extends RedissonMap implements RMapCac private static final RedisCommand EVAL_REMOVE_VALUE = new RedisCommand("EVAL", new BooleanReplayConvertor(), 5, ValueType.MAP); private static final RedisCommand EVAL_PUT_TTL = new RedisCommand("EVAL", 9, ValueType.MAP, ValueType.MAP_VALUE); private static final RedisCommand EVAL_FAST_PUT_TTL = new RedisCommand("EVAL", new BooleanReplayConvertor(), 9, ValueType.MAP, ValueType.MAP_VALUE); + private static final RedisCommand EVAL_FAST_PUT_TTL_IF_ABSENT = new RedisCommand("EVAL", new BooleanReplayConvertor(), 10, ValueType.MAP, ValueType.MAP_VALUE); private static final RedisCommand EVAL_GET_TTL = new RedisCommand("EVAL", 7, ValueType.MAP_KEY, ValueType.MAP_VALUE); private static final RedisCommand EVAL_CONTAINS_KEY = new RedisCommand("EVAL", new BooleanReplayConvertor(), 7, ValueType.MAP_KEY); static final RedisCommand EVAL_CONTAINS_VALUE = new RedisCommand("EVAL", new BooleanReplayConvertor(), 7, ValueType.MAP_VALUE); @@ -686,6 +687,99 @@ public class RedissonMapCache extends RedissonMap implements RMapCac + "return 1; ", Arrays.asList(getName(key), getTimeoutSetNameByKey(key), getIdleSetNameByKey(key)), System.currentTimeMillis(), key, value); } + + @Override + public boolean fastPutIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit) { + return fastPutIfAbsent(key, value, ttl, ttlUnit, 0, null); + } + + @Override + public boolean fastPutIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit) { + return get(fastPutIfAbsentAsync(key, value, ttl, ttlUnit, maxIdleTime, maxIdleUnit)); + } + + @Override + public RFuture fastPutIfAbsentAsync(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit) { + if (ttl < 0) { + throw new IllegalArgumentException("ttl can't be negative"); + } + if (maxIdleTime < 0) { + throw new IllegalArgumentException("maxIdleTime can't be negative"); + } + if (ttl == 0 && maxIdleTime == 0) { + return fastPutIfAbsentAsync(key, value); + } + + if (ttl > 0 && ttlUnit == null) { + throw new NullPointerException("ttlUnit param can't be null"); + } + + if (maxIdleTime > 0 && maxIdleUnit == null) { + throw new NullPointerException("maxIdleUnit param can't be null"); + } + + long ttlTimeout = 0; + if (ttl > 0) { + ttlTimeout = System.currentTimeMillis() + ttlUnit.toMillis(ttl); + } + + long maxIdleTimeout = 0; + long maxIdleDelta = 0; + if (maxIdleTime > 0) { + maxIdleDelta = maxIdleUnit.toMillis(maxIdleTime); + maxIdleTimeout = System.currentTimeMillis() + maxIdleDelta; + } + + return commandExecutor.evalWriteAsync(getName(key), codec, EVAL_FAST_PUT_TTL_IF_ABSENT, + "local insertable = false; " + + "local value = redis.call('hget', KEYS[1], ARGV[5]); " + + "if value == false then " + + "insertable = true; " + + "else " + + "if insertable == false then " + + "local t, val = struct.unpack('dLc0', value); " + + "local expireDate = 92233720368547758; " + + "local expireDateScore = redis.call('zscore', KEYS[2], ARGV[5]); " + + "if expireDateScore ~= false then " + + "expireDate = tonumber(expireDateScore) " + + "end; " + + "if t ~= 0 then " + + "local expireIdle = redis.call('zscore', KEYS[3], ARGV[5]); " + + "if expireIdle ~= false then " + + "expireDate = math.min(expireDate, tonumber(expireIdle)) " + + "end; " + + "end; " + + "if expireDate <= tonumber(ARGV[1]) then " + + "insertable = true; " + + "end; " + + "end; " + + "end; " + + + "if insertable == true then " + // ttl + + "if tonumber(ARGV[2]) > 0 then " + + "redis.call('zadd', KEYS[2], ARGV[2], ARGV[5]); " + + "else " + + "redis.call('zrem', KEYS[2], ARGV[5]); " + + "end; " + + // idle + + "if tonumber(ARGV[3]) > 0 then " + + "redis.call('zadd', KEYS[3], ARGV[3], ARGV[5]); " + + "else " + + "redis.call('zrem', KEYS[3], ARGV[5]); " + + "end; " + + // value + + "local val = struct.pack('dLc0', ARGV[4], string.len(ARGV[6]), ARGV[6]); " + + "redis.call('hset', KEYS[1], ARGV[5], val); " + + + "return 1; " + + "else " + + "return 0; " + + "end; ", + Arrays.asList(getName(key), getTimeoutSetNameByKey(key), getIdleSetNameByKey(key)), System.currentTimeMillis(), ttlTimeout, maxIdleTimeout, maxIdleDelta, key, value); + } @Override public RFuture replaceAsync(K key, V oldValue, V newValue) { @@ -912,5 +1006,4 @@ public class RedissonMapCache extends RedissonMap implements RMapCac "return result;", Arrays.asList(getName(), getTimeoutSetName(), getIdleSetName()), System.currentTimeMillis()); } - } diff --git a/redisson/src/main/java/org/redisson/api/RMapCache.java b/redisson/src/main/java/org/redisson/api/RMapCache.java index 5a181d9c4..c59be9beb 100644 --- a/redisson/src/main/java/org/redisson/api/RMapCache.java +++ b/redisson/src/main/java/org/redisson/api/RMapCache.java @@ -161,6 +161,53 @@ public interface RMapCache extends RMap, RMapCacheAsync { * false if key already exists in the hash and the value was updated. */ boolean fastPut(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); + + /** + * If the specified key is not already associated + * with a value, associate it with the given value. + *

+ * Stores value mapped by key with specified time to live. + * Entry expires after specified time to live. + *

+ * Works faster than usual {@link #putIfAbsent(Object, Object, long, TimeUnit)} + * as it not returns previous value. + * + * @param key - map key + * @param value - map value + * @param ttl - time to live for key\value entry. + * If 0 then stores infinitely. + * @param ttlUnit - time unit + * @return true if key is a new key in the hash and value was set. + * false if key already exists in the hash + */ + boolean fastPutIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit); + + /** + * If the specified key is not already associated + * with a value, associate it with the given value. + *

+ * Stores value mapped by key with specified time to live and max idle time. + * Entry expires when specified time to live or max idle time has expired. + *

+ * Works faster than usual {@link #putIfAbsent(Object, Object, long, TimeUnit, long, TimeUnit)} + * as it not returns previous value. + * + * @param key - map key + * @param value - map value + * @param ttl - time to live for key\value entry. + * If 0 then time to live doesn't affect entry expiration. + * @param ttlUnit - time unit + * @param maxIdleTime - max idle time for key\value entry. + * If 0 then max idle time doesn't affect entry expiration. + * @param maxIdleUnit - time unit + *

+ * if maxIdleTime and ttl params are equal to 0 + * then entry stores infinitely. + * + * @return true if key is a new key in the hash and value was set. + * false if key already exists in the hash. + */ + boolean fastPutIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); /** * Returns the number of entries in cache. diff --git a/redisson/src/main/java/org/redisson/api/RMapCacheAsync.java b/redisson/src/main/java/org/redisson/api/RMapCacheAsync.java index 979590ebb..8eb0a8698 100644 --- a/redisson/src/main/java/org/redisson/api/RMapCacheAsync.java +++ b/redisson/src/main/java/org/redisson/api/RMapCacheAsync.java @@ -163,6 +163,32 @@ public interface RMapCacheAsync extends RMapAsync { * @return true if value has been set successfully */ RFuture fastPutAsync(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); + + /** + * If the specified key is not already associated + * with a value, associate it with the given value. + *

+ * Stores value mapped by key with specified time to live and max idle time. + * Entry expires when specified time to live or max idle time has expired. + *

+ * Works faster than usual {@link #putIfAbsentAsync(Object, Object, long, TimeUnit, long, TimeUnit)} + * as it not returns previous value. + * + * @param key - map key + * @param value - map value + * @param ttl - time to live for key\value entry. + * If 0 then time to live doesn't affect entry expiration. + * @param ttlUnit - time unit + * @param maxIdleTime - max idle time for key\value entry. + * If 0 then max idle time doesn't affect entry expiration. + * @param maxIdleUnit - time unit + *

+ * if maxIdleTime and ttl params are equal to 0 + * then entry stores infinitely. + * + * @return previous associated value + */ + RFuture fastPutIfAbsentAsync(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); /** * Returns the number of entries in cache. diff --git a/redisson/src/main/resources/META-INF/spring.schemas b/redisson/src/main/resources/META-INF/spring.schemas index 1753d1003..274331f67 100644 --- a/redisson/src/main/resources/META-INF/spring.schemas +++ b/redisson/src/main/resources/META-INF/spring.schemas @@ -1,2 +1,2 @@ -http\://redisson.org/schema/redisson.xsd=org/redisson/spring/support/redisson-1.0.xsd -http\://redisson.org/schema/redisson-1.0.xsd=org/redisson/spring/support/redisson-1.0.xsd +http\://redisson.org/schema/redisson/redisson.xsd=org/redisson/spring/support/redisson-1.0.xsd +http\://redisson.org/schema/redisson/redisson-1.0.xsd=org/redisson/spring/support/redisson-1.0.xsd diff --git a/redisson/src/test/java/org/redisson/ClusterRunner.java b/redisson/src/test/java/org/redisson/ClusterRunner.java index b1c11228b..abca5b170 100644 --- a/redisson/src/test/java/org/redisson/ClusterRunner.java +++ b/redisson/src/test/java/org/redisson/ClusterRunner.java @@ -17,9 +17,10 @@ import java.util.List; public class ClusterRunner { private final LinkedHashMap nodes = new LinkedHashMap<>(); + private final LinkedHashMap masters = new LinkedHashMap<>(); public ClusterRunner addNode(RedisRunner runner) { - nodes.put(runner, getRandomId()); + nodes.putIfAbsent(runner, getRandomId()); if (!runner.hasOption(RedisRunner.REDIS_OPTIONS.CLUSTER_ENABLED)) { runner.clusterEnabled(true); } @@ -36,11 +37,20 @@ public class ClusterRunner { return this; } + public ClusterRunner addNode(RedisRunner master, RedisRunner... slaves) { + addNode(master); + for (RedisRunner slave : slaves) { + addNode(slave); + masters.put(nodes.get(slave), nodes.get(master)); + } + return this; + } + public List run() throws IOException, InterruptedException, RedisRunner.FailedToStartRedisException { ArrayList processes = new ArrayList<>(); for (RedisRunner runner : nodes.keySet()) { List options = getClusterConfig(runner); - String confFile = runner.defaultDir() + File.pathSeparator + nodes.get(runner) + ".conf"; + String confFile = runner.dir() + File.separatorChar + nodes.get(runner) + ".conf"; System.out.println("WRITING CONFIG: for " + nodes.get(runner)); try (PrintWriter printer = new PrintWriter(new FileWriter(confFile))) { options.stream().forEach((line) -> { @@ -64,13 +74,20 @@ public class ClusterRunner { List nodeConfig = new ArrayList<>(); int c = 0; for (RedisRunner node : nodes.keySet()) { + String nodeId = nodes.get(node); StringBuilder sb = new StringBuilder(); String nodeAddr = node.getInitialBindAddr() + ":" + node.getPort(); - sb.append(nodes.get(node)).append(" "); + sb.append(nodeId).append(" "); sb.append(nodeAddr).append(" "); sb.append(me.equals(nodeAddr) ? "myself," - : "").append("master -").append(" "); + : ""); + if (!masters.containsKey(nodeId)) { + sb.append("master -"); + } else { + sb.append("slave ").append(masters.get(nodeId)); + } + sb.append(" "); sb.append("0").append(" "); sb.append(me.equals(nodeAddr) ? "0" diff --git a/redisson/src/test/java/org/redisson/RedisRunner.java b/redisson/src/test/java/org/redisson/RedisRunner.java index e0990ffae..5499bd5ad 100644 --- a/redisson/src/test/java/org/redisson/RedisRunner.java +++ b/redisson/src/test/java/org/redisson/RedisRunner.java @@ -191,6 +191,7 @@ public class RedisRunner { protected static RedisRunner.RedisProcess defaultRedisInstance; private static int defaultRedisInstanceExitCode; + private String path = ""; private String defaultDir = Paths.get("").toString(); private boolean nosave = false; private boolean randomDir = false; @@ -459,6 +460,7 @@ public class RedisRunner { public RedisRunner dir(String dir) { if (!randomDir) { addConfigOption(REDIS_OPTIONS.DIR, dir); + this.path = dir; } return this; } @@ -824,6 +826,10 @@ public class RedisRunner { public String defaultDir() { return this.defaultDir; } + + public String dir() { + return this.path; + } public String getInitialBindAddr() { return bindAddr.size() > 0 ? bindAddr.get(0) : "localhost"; @@ -849,7 +855,7 @@ public class RedisRunner { public boolean deleteClusterFile() { File f = new File(clusterFile); - if (f.exists()) { + if (f.exists() && isRandomDir()) { System.out.println("REDIS RUNNER: Deleting cluster config file " + f.getAbsolutePath()); return f.delete(); } diff --git a/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java b/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java index 3c3d95904..6a3c095db 100644 --- a/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java +++ b/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java @@ -838,4 +838,29 @@ public class RedissonMapCacheTest extends BaseTest { this.testField = testField; } } + + @Test + public void testFastPutIfAbsentWithTTL() throws Exception { + RMapCache map = redisson.getMapCache("simpleTTL"); + SimpleKey key = new SimpleKey("1"); + SimpleValue value = new SimpleValue("2"); + map.fastPutIfAbsent(key, value, 1, TimeUnit.SECONDS); + assertThat(map.fastPutIfAbsent(key, new SimpleValue("3"), 1, TimeUnit.SECONDS)).isFalse(); + assertThat(map.get(key)).isEqualTo(value); + + Thread.sleep(1100); + + assertThat(map.fastPutIfAbsent(key, new SimpleValue("3"), 1, TimeUnit.SECONDS)).isTrue(); + assertThat(map.get(key)).isEqualTo(new SimpleValue("3")); + + assertThat(map.fastPutIfAbsent(key, new SimpleValue("4"), 1, TimeUnit.SECONDS)).isFalse(); + assertThat(map.get(key)).isEqualTo(new SimpleValue("3")); + + Thread.sleep(1100); + assertThat(map.fastPutIfAbsent(key, new SimpleValue("4"), 1, TimeUnit.SECONDS, 500, TimeUnit.MILLISECONDS)).isTrue(); + + Thread.sleep(550); + assertThat(map.fastPutIfAbsent(key, new SimpleValue("5"), 1, TimeUnit.SECONDS, 500, TimeUnit.MILLISECONDS)).isTrue(); + + } }