DATAREDIS-743 - Add Reactive SCAN, HSCAN, SSCAN, and ZSCAN support.

We now provide reactive SCAN support for keys, hashes, sets, and sorted sets. Scanning uses a cursor-based Flux and takes subscriber demand into account to issue the according scan commands once a batch of keys/values/entries was emitted. Specifying a SCAN limit controls the batch (page) size and can be used to optimize the number of roundtrips between the application and Redis.

Original Pull Request: #343
This commit is contained in:
Mark Paluch
2018-05-15 18:09:00 +02:00
committed by Christoph Strobl
parent a9062dfe33
commit d4b06c2984
33 changed files with 628 additions and 28 deletions

View File

@@ -28,6 +28,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.data.redis.core.ScanOptions;
/**
* @author Christoph Strobl
@@ -233,6 +234,18 @@ public class LettuceReactiveHashCommandsTests extends LettuceReactiveCommandsTes
.verifyComplete();
}
@Test // DATAREDIS-743
public void hScanShouldIterateOverHash() {
nativeCommands.hset(KEY_1, FIELD_1, VALUE_1);
nativeCommands.hset(KEY_1, FIELD_2, VALUE_2);
nativeCommands.hset(KEY_1, FIELD_3, VALUE_3);
StepVerifier.create(connection.hashCommands().hScan(KEY_1_BBUFFER, ScanOptions.scanOptions().count(1).build())) //
.expectNextCount(3) //
.verifyComplete();
}
@Test // DATAREDIS-698
public void hStrLenReturnsFieldLength() {

View File

@@ -38,6 +38,7 @@ import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.test.util.MinimumRedisVersionRule;
import org.springframework.test.annotation.IfProfileValue;
@@ -95,6 +96,26 @@ public class LettuceReactiveKeyCommandsTests extends LettuceReactiveCommandsTest
.expectNextCount(3).verifyComplete();
}
@Test // DATAREDIS-743
public void scanShouldShouldIterateOverKeyspace() {
nativeCommands.set(KEY_1, VALUE_2);
nativeCommands.set(KEY_2, VALUE_2);
nativeCommands.set(KEY_3, VALUE_3);
nativeCommands.set(VALUE_1, KEY_1);
nativeCommands.set(VALUE_2, KEY_2);
nativeCommands.set(VALUE_3, KEY_3);
StepVerifier.create(connection.keyCommands().scan(ScanOptions.scanOptions().count(2).build())) //
.expectNextCount(6) //
.verifyComplete();
StepVerifier.create(connection.keyCommands().scan(ScanOptions.scanOptions().count(2).match("key*").build())) //
.expectNextCount(3) //
.verifyComplete();
}
@Test // DATAREDIS-525
public void randomKeyShouldReturnAnyKey() {

View File

@@ -27,6 +27,7 @@ import reactor.test.StepVerifier;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.data.redis.core.ScanOptions;
public class LettuceReactiveSetCommandsTests extends LettuceReactiveCommandsTestsBase {
@@ -220,6 +221,21 @@ public class LettuceReactiveSetCommandsTests extends LettuceReactiveCommandsTest
.verifyComplete();
}
@Test // DATAREDIS-743
public void sScanShouldIterateOverSet() {
nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3);
StepVerifier.create(connection.setCommands().sScan(KEY_1_BBUFFER)) //
.expectNextCount(3) //
.verifyComplete();
StepVerifier
.create(connection.setCommands().sScan(KEY_1_BBUFFER, ScanOptions.scanOptions().match("value-3").build())) //
.expectNext(VALUE_3_BBUFFER) //
.verifyComplete();
}
@Test // DATAREDIS-525
public void sRandMemberReturnsRandomMember() {

View File

@@ -28,7 +28,7 @@ import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.DefaultTuple;
import org.springframework.data.redis.test.util.LettuceRedisClientProvider;
import org.springframework.data.redis.core.ScanOptions;
/**
* @author Christoph Strobl
@@ -276,6 +276,22 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes
.verifyComplete();
}
@Test // DATAREDIS-743
public void zScanShouldIterateOverSortedSet() {
nativeCommands.zadd(KEY_1, 1D, VALUE_1);
nativeCommands.zadd(KEY_1, 2D, VALUE_2);
nativeCommands.zadd(KEY_1, 3D, VALUE_3);
StepVerifier.create(connection.zSetCommands().zScan(KEY_1_BBUFFER, ScanOptions.scanOptions().count(1).build())) //
.expectNextCount(3).verifyComplete();
StepVerifier
.create(connection.zSetCommands().zScan(KEY_1_BBUFFER, ScanOptions.scanOptions().match(VALUE_2).build())) //
.expectNext(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D)) //
.verifyComplete();
}
@Test // DATAREDIS-525
public void zCountShouldCountValuesInRange() {

View File

@@ -382,6 +382,31 @@ public class DefaultReactiveHashOperationsIntegrationTests<K, HK, HV> {
.verifyComplete();
}
@Test // DATAREDIS-743
public void scan() {
assumeTrue(hashKeyFactory instanceof StringObjectFactory && hashValueFactory instanceof StringObjectFactory);
K key = keyFactory.instance();
HK hashkey1 = hashKeyFactory.instance();
HV hashvalue1 = hashValueFactory.instance();
HK hashkey2 = hashKeyFactory.instance();
HV hashvalue2 = hashValueFactory.instance();
putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2);
StepVerifier.create(hashOperations.scan(key).buffer(2)) //
.consumeNextWith(list -> {
Entry<HK, HV> entry1 = Collections.singletonMap(hashkey1, hashvalue1).entrySet().iterator().next();
Entry<HK, HV> entry2 = Collections.singletonMap(hashkey2, hashvalue2).entrySet().iterator().next();
assertThat(list).containsExactlyInAnyOrder(entry1, entry2);
}) //
.verifyComplete();
}
@Test // DATAREDIS-602
public void delete() {

View File

@@ -310,6 +310,25 @@ public class DefaultReactiveSetOperationsIntegrationTests<K, V> {
.consumeNextWith(actual -> assertThat(actual).isIn(value1, value2)).expectNextCount(1).verifyComplete();
}
@Test // DATAREDIS-743
public void scan() {
assumeFalse(valueFactory instanceof ByteBufferObjectFactory);
K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();
StepVerifier.create(setOperations.add(key, value1, value2)) //
.expectNext(2L) //
.verifyComplete();
StepVerifier.create(setOperations.scan(key)) //
.consumeNextWith(actual -> assertThat(actual).isIn(value1, value2)) //
.expectNextCount(1) //
.verifyComplete();
}
@Test // DATAREDIS-602
public void randomMember() {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.core;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import reactor.test.StepVerifier;
@@ -389,6 +390,29 @@ public class DefaultReactiveZSetOperationsIntegrationTests<K, V> {
.verifyComplete();
}
@Test // DATAREDIS-743
public void scan() {
assumeFalse(valueFactory instanceof ByteBufferObjectFactory);
K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();
StepVerifier.create(zSetOperations.add(key, value1, 42.1)) //
.expectNext(true) //
.verifyComplete();
StepVerifier.create(zSetOperations.add(key, value2, 10)) //
.expectNext(true) //
.verifyComplete();
StepVerifier.create(zSetOperations.scan(key)) //
.consumeNextWith(actual -> assertThat(actual).isIn(new DefaultTypedTuple<>(value1, 42.1),
new DefaultTypedTuple<>(value2, 10d))) //
.expectNextCount(1) //
.verifyComplete();
}
@Test // DATAREDIS-602
public void count() {

View File

@@ -25,6 +25,8 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.AfterClass;
import org.junit.Before;
@@ -116,6 +118,23 @@ public class ReactiveRedisTemplateIntegrationTests<K, V> {
StepVerifier.create(redisTemplate.hasKey(key)).expectNext(true).verifyComplete();
}
@Test // DATAREDIS-743
public void scan() {
assumeFalse(valueFactory.instance() instanceof Person);
Map<K, V> tuples = new HashMap<>();
tuples.put(keyFactory.instance(), valueFactory.instance());
tuples.put(keyFactory.instance(), valueFactory.instance());
tuples.put(keyFactory.instance(), valueFactory.instance());
StepVerifier.create(redisTemplate.opsForValue().multiSet(tuples)).expectNext(true).verifyComplete();
StepVerifier.create(redisTemplate.scan().collectList()) //
.consumeNextWith(actual -> assertThat(actual).containsAll(tuples.keySet())) //
.verifyComplete();
}
@Test // DATAREDIS-602
public void type() {