diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index f15f39b88f..b300b57322 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -#Thu Nov 07 09:47:27 CET 2024 -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +#Thu Jul 17 14:00:56 CEST 2025 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index a4820853f1..c97e40ae1f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { triggers { pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "spring-data-keyvalue/main", threshold: hudson.model.Result.SUCCESS) + upstream(upstreamProjects: "spring-data-keyvalue/3.5.x", threshold: hudson.model.Result.SUCCESS) } options { diff --git a/pom.xml b/pom.xml index 8c967f3d27..80016d8768 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 org.springframework.data spring-data-redis - 3.5.0 + 3.5.5-SNAPSHOT Spring Data Redis Spring Data module for Redis @@ -14,12 +14,12 @@ org.springframework.data.build spring-data-parent - 3.5.0 + 3.5.5-SNAPSHOT - 3.5.0 - 3.5.0 + 3.5.5-SNAPSHOT + 3.5.5-SNAPSHOT 1.9.4 1.4.21 2.11.1 @@ -388,7 +388,19 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + diff --git a/src/main/antora/modules/ROOT/pages/observability.adoc b/src/main/antora/modules/ROOT/pages/observability.adoc index e3a43ae122..f7b53ada60 100644 --- a/src/main/antora/modules/ROOT/pages/observability.adoc +++ b/src/main/antora/modules/ROOT/pages/observability.adoc @@ -16,7 +16,7 @@ class ObservabilityConfiguration { public ClientResources clientResources(ObservationRegistry observationRegistry) { return ClientResources.builder() - .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) + .tracing(new MicrometerTracing(observationRegistry, "my-redis-cache")) .build(); } @@ -31,15 +31,20 @@ class ObservabilityConfiguration { } ---- +NOTE: When using Spring Boot, `LettuceMetricsAutoConfiguration` configures Lettuce's `MicrometerCommandLatencyRecorder`. +Depending on whether you want only Metrics or Metrics and Tracing, you might want to exclude this auto-configuration class in your application. + +NOTE: Use Lettuce's built-in `MicrometerTracing` as `MicrometerTracingAdapter` has been deprecated for removal in future releases. + See also https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] for further reference. [[observability-metrics]] == Observability - Metrics -Below you can find a list of all metrics declared by this project. +Below you can find a list of all metrics recorded by `MicrometerTracingAdapter`. [[observability-metrics-redis-command-observation]] -== Redis Command Observation +=== Redis Command Observation ____ Timer created around a Redis command execution. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc index fb1a08c0f8..166786b1d0 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc @@ -183,7 +183,7 @@ NOTE: Custom conversions have no effect on index resolution. xref:redis/redis-re == Customizing Type Mapping If you want to avoid writing the entire Java class name as type information and would rather like to use a key, you can use the `@TypeAlias` annotation on the entity class being persisted. -If you need to customize the mapping even more, look at the https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/convert/TypeInformationMapper.html[`TypeInformationMapper`] interface. +If you need to customize the mapping even more, look at the {spring-data-commons-javadoc-base}/org/springframework/data/convert/TypeInformationMapper.html[`TypeInformationMapper`] interface. An instance of that interface can be configured at the `DefaultRedisTypeMapper`, which can be configured on `MappingRedisConverter`. The following example shows how to define a type alias for an entity: diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index 10dd13d032..a42895b13e 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -3,21 +3,22 @@ prerelease: ${antora-component.prerelease} asciidoc: attributes: - copyright-year: ${current.year} - version: ${project.version} - springversionshort: ${spring.short} - springversion: ${spring} attribute-missing: 'warn' - commons: ${springdata.commons.docs} - lettuce: ${lettuce} - jedis: ${jedis} + chomp: 'all' + version: '${project.version}' + copyright-year: '${current.year}' + springversionshort: '${spring.short}' + springversion: '${spring}' + commons: '${springdata.commons.docs}' + lettuce: '${lettuce}' + jedis: '${jedis}' include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference - spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ - springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api + spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}' + spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' + springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}' spring-framework-docs: '{springdocsurl}' + springjavadocurl: '${documentation.spring-javadoc-url}' spring-framework-javadoc: '{springjavadocurl}' - springhateoasversion: ${spring-hateoas} - releasetrainversion: ${releasetrain} + springhateoasversion: '${spring-hateoas}' + releasetrainversion: '${releasetrain}' store: Redis diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index 3354cf9af1..877a787f5e 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -244,9 +244,10 @@ default Mono touch(Collection keys) { Flux, Long>> touch(Publisher> keys); /** - * Find all keys matching the given {@literal pattern}.
- * It is recommended to use {@link #scan(ScanOptions)} to iterate over the keyspace as {@link #keys(ByteBuffer)} is a - * non-interruptible and expensive Redis operation. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java index 4319dd8705..e79cf2c0f8 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java @@ -122,7 +122,10 @@ default Boolean exists(byte[] key) { Long touch(byte[]... keys); /** - * Find all keys matching the given {@code pattern}. + * Retrieve all keys matching the given pattern. + *

+ * IMPORTANT: The {@literal KEYS} command is non-interruptible and scans the entire keyspace which + * may cause performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return empty {@link Set} if no match found. {@literal null} when used in pipeline / transaction. diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index dfcc585130..dd483bdf89 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -185,7 +185,10 @@ interface StringTuple extends Tuple { Long touch(String... keys); /** - * Find all keys matching the given {@code pattern}. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index 01cb4badec..93a4edd6e6 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -761,38 +761,16 @@ public void stop() { if (this.state.compareAndSet(State.STARTED, State.STOPPING)) { if (getUsePool() && !isRedisClusterAware()) { - if (this.pool != null) { - try { - this.pool.close(); - this.pool = null; - } catch (Exception ex) { - log.warn("Cannot properly close Jedis pool", ex); - } - } - } - - ClusterCommandExecutor clusterCommandExecutor = this.clusterCommandExecutor; - - if (clusterCommandExecutor != null) { - try { - clusterCommandExecutor.destroy(); - this.clusterCommandExecutor = null; - } catch (Exception ex) { - throw new RuntimeException(ex); - } + dispose(pool); + pool = null; } - if (this.cluster != null) { + dispose(clusterCommandExecutor); + clusterCommandExecutor = null; - this.topologyProvider = null; - - try { - this.cluster.close(); - this.cluster = null; - } catch (Exception ex) { - log.warn("Cannot properly close Jedis cluster", ex); - } - } + dispose(cluster); + topologyProvider = null; + cluster = null; this.state.set(State.STOPPED); } @@ -882,6 +860,36 @@ public void destroy() { state.set(State.DESTROYED); } + private void dispose(@Nullable ClusterCommandExecutor commandExecutor) { + if (commandExecutor != null) { + try { + commandExecutor.destroy(); + } catch (Exception ex) { + log.warn("Cannot properly close cluster command executor", ex); + } + } + } + + private void dispose(@Nullable JedisCluster cluster) { + if (cluster != null) { + try { + cluster.close(); + } catch (Exception ex) { + log.warn("Cannot properly close Jedis cluster", ex); + } + } + } + + private void dispose(@Nullable Pool pool) { + if (pool != null) { + try { + pool.close(); + } catch (Exception ex) { + log.warn("Cannot properly close Jedis pool", ex); + } + } + } + @Override public RedisConnection getConnection() { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index befdfffe50..8941695eac 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -116,6 +116,7 @@ * @author Chris Bono * @author John Blum * @author Zhian Chen + * @author UHyeon Jeong */ public class LettuceConnectionFactory implements RedisConnectionFactory, ReactiveRedisConnectionFactory, InitializingBean, DisposableBean, SmartLifecycle { @@ -973,30 +974,23 @@ public void stop() { resetConnection(); + dispose(clusterCommandExecutor); + clusterCommandExecutor = null; + dispose(connectionProvider); connectionProvider = null; dispose(reactiveConnectionProvider); reactiveConnectionProvider = null; - if (client != null) { - try { - Duration quietPeriod = clientConfiguration.getShutdownQuietPeriod(); - Duration timeout = clientConfiguration.getShutdownTimeout(); - - client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS); - client = null; - } catch (Exception ex) { - if (log.isWarnEnabled()) { - log.warn(ClassUtils.getShortName(client.getClass()) + " did not shut down gracefully.", ex); - } - } - } + dispose(client); + client = null; } state.set(State.STOPPED); } + @Override public boolean isRunning() { return State.STARTED.equals(this.state.get()); @@ -1014,20 +1008,18 @@ public void afterPropertiesSet() { public void destroy() { stop(); - this.client = null; + this.state.set(State.DESTROYED); + } - ClusterCommandExecutor clusterCommandExecutor = this.clusterCommandExecutor; + private void dispose(@Nullable ClusterCommandExecutor commandExecutor) { - if (clusterCommandExecutor != null) { + if (commandExecutor != null) { try { - clusterCommandExecutor.destroy(); - this.clusterCommandExecutor = null; + commandExecutor.destroy(); } catch (Exception ex) { log.warn("Cannot properly close cluster command executor", ex); } } - - this.state.set(State.DESTROYED); } private void dispose(@Nullable LettuceConnectionProvider connectionProvider) { @@ -1043,6 +1035,22 @@ private void dispose(@Nullable LettuceConnectionProvider connectionProvider) { } } + private void dispose(@Nullable AbstractRedisClient client) { + + if (client != null) { + try { + Duration quietPeriod = clientConfiguration.getShutdownQuietPeriod(); + Duration timeout = clientConfiguration.getShutdownTimeout(); + + client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (Exception ex) { + if (log.isWarnEnabled()) { + log.warn(ClassUtils.getShortName(client.getClass()) + " did not shut down gracefully.", ex); + } + } + } + } + @Override public RedisConnection getConnection() { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java index 086c9ce763..23d45c7286 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingConnectionProvider.java @@ -30,11 +30,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; + import org.springframework.beans.factory.DisposableBean; import org.springframework.data.redis.connection.PoolException; import org.springframework.util.Assert; @@ -56,6 +58,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Asmir Mustafic + * @author UHyeon Jeong * @since 2.0 * @see #getConnection(Class) */ @@ -63,6 +66,7 @@ class LettucePoolingConnectionProvider implements LettuceConnectionProvider, Red private static final Log log = LogFactory.getLog(LettucePoolingConnectionProvider.class); + private final AtomicBoolean disposed = new AtomicBoolean(); private final LettuceConnectionProvider connectionProvider; private final GenericObjectPoolConfig> poolConfig; private final Map, GenericObjectPool>> poolRef = new ConcurrentHashMap<>( @@ -207,6 +211,10 @@ public CompletableFuture releaseAsync(StatefulConnection connection) @Override public void destroy() throws Exception { + if (!disposed.compareAndSet(false, true)) { + return; + } + List> futures = new ArrayList<>(); if (!poolRef.isEmpty() || !asyncPoolRef.isEmpty()) { log.warn("LettucePoolingConnectionProvider contains unreleased connections"); @@ -250,4 +258,5 @@ public void destroy() throws Exception { pools.clear(); } + } diff --git a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java index f96c8c0d39..01ebb31246 100644 --- a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java @@ -38,7 +38,10 @@ public interface ClusterOperations { /** - * Get all keys located at given node. + * Retrieve all keys located at given node matching the given pattern. + *

+ * IMPORTANT: The {@literal KEYS} command is non-interruptible and scans the entire keyspace which + * may cause performance issues. * * @param node must not be {@literal null}. * @param pattern diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java index 686277f0df..55f126b636 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java @@ -264,9 +264,10 @@ default Mono>> listenToPatternLater(String... Mono type(K key); /** - * Find all keys matching the given {@code pattern}.
- * IMPORTANT: It is recommended to use {@link #scan()} to iterate over the keyspace as - * {@link #keys(Object)} is a non-interruptible and expensive Redis operation. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return the {@link Flux} emitting matching keys one by one. diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index 1454fd33c2..52d08ce6e8 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -45,25 +45,43 @@ public enum RedisCommand { // -- A APPEND("rw", 2, 2), // AUTH("rw", 1, 1), // + // -- B BGREWRITEAOF("r", 0, 0, "bgwriteaof"), // BGSAVE("r", 0, 0), // BITCOUNT("r", 1, 3), // + BITFIELD("rw", 1), // + BITFIELD_RO("r", 1), BITOP("rw", 3), // BITPOS("r", 2, 4), // + BLMOVE("rw", 4), // + BLMPOP("rw", 4), // BLPOP("rw", 2), // BRPOP("rw", 2), // BRPOPLPUSH("rw", 3), // + BZMPOP("rw", 3), // + BZPOPMAX("rw", 2), // + BZPOPMIN("rw", 2), // + // -- C + CLIENT_GETREDIR("r", 0, 0), // + CLIENT_ID("r", 0, 0), // + CLIENT_INFO("r", 0, 0), // CLIENT_KILL("rw", 1, 1), // CLIENT_LIST("r", 0, 0), // CLIENT_GETNAME("r", 0, 0), // CLIENT_PAUSE("rw", 1, 1), // + CLIENT_SETINFO("w", 1), // CLIENT_SETNAME("w", 1, 1), // + CLIENT_NO_EVICT("w", 1, 1, "client no-evict"), // + CLIENT_NO_TOUCH("w", 1, 1, "client no-touch"), // + CLIENT_TRACKING("rw", 1), // CONFIG_GET("r", 1, 1, "getconfig"), // CONFIG_REWRITE("rw", 0, 0), // CONFIG_SET("w", 2, 2, "setconfig"), // CONFIG_RESETSTAT("w", 0, 0, "resetconfigstats"), // + COPY("rw", 2), // + // -- D DBSIZE("r", 0, 0), // DECR("w", 1, 1), // @@ -71,39 +89,62 @@ public enum RedisCommand { DEL("rw", 1), // DISCARD("rw", 0, 0), // DUMP("r", 1, 1), // + // -- E ECHO("r", 1, 1), // EVAL("rw", 2), // + EVAL_RO("r", 2), // EVALSHA("rw", 2), // + EVALSHA_RO("r", 2), // EXEC("rw", 0, 0), // EXISTS("r", 1, 1), // EXPIRE("rw", 2), // EXPIREAT("rw", 2), // + EXPIRETIME("r", 1), // + // -- F + FCALL("rw", 2), // + FCALL_RO("r", 2), // FLUSHALL("w", 0, 0), // FLUSHDB("w", 0, 0), // + FUNCTION_DELETE("w", 1), // + FUNCTION_DUMP("w", 0, 0), // + FUNCTION_FLUSH("w", 0, 0), // + FUNCTION_KILL("w", 0, 0), // + // -- G GET("r", 1, 1), // GETBIT("r", 2, 2), // + GETDEL("rw", 1), // + GETEX("rw", 1), // GETRANGE("r", 3, 3), // GETSET("rw", 2, 2), // GEOADD("w", 3), // GEODIST("r", 2), // GEOHASH("r", 2), // GEOPOS("r", 2), // - GEORADIUS("r", 4), // - GEORADIUSBYMEMBER("r", 3), // + GEORADIUS("rw", 4), // + GEORADIUS_RO("r", 4), // + GEORADIUSBYMEMBER("rw", 3), // + GEORADIUSBYMEMBER_RO("r", 3), // + GEOSEARCH("r", 1), // + GEOSEARCH_STORE("rw", 1), // + // -- H HDEL("rw", 2), // + HELLO("rw", 0, 0), // HEXISTS("r", 2, 2), // HGET("r", 2, 2), // HGETALL("r", 1, 1), // + HGETDEL("rw", 2), // + HGETEX("rw", 2), // HINCRBY("rw", 3, 3), // HINCBYFLOAT("rw", 3, 3), // HKEYS("r", 1), // HLEN("r", 1), // HMGET("r", 2), // HMSET("w", 3), // + HPOP("rw", 3), HSET("w", 3, 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // @@ -111,27 +152,39 @@ public enum RedisCommand { HEXPIREAT("w", 5), // HPEXPIRE("w", 5), // HPEXPIREAT("w", 5), // + HPEXPIRETIME("r", 4), // HPERSIST("w", 4), // HTTL("r", 4), // HPTTL("r", 4), // + HSCAN("r", 2), // + HSTRLEN("r", 2), // + // -- I INCR("rw", 1), // + INCRBY("rw", 2, 2), // INCRBYFLOAT("rw", 2, 2), // INFO("r", 0), // + // -- K KEYS("r", 1), // + // -- L + LCS("r", 2), // LASTSAVE("r", 0), // LINDEX("r", 2, 2), // LINSERT("rw", 4, 4), // LLEN("r", 1, 1), // + LMOVE("rw", 2), // + LMPOP("rw", 2), // LPOP("rw", 1, 1), // + LPOS("r", 2), // LPUSH("rw", 2), // LPUSHX("rw", 2), // LRANGE("r", 3, 3), // LREM("rw", 3, 3), // LSET("w", 3, 3), // LTRIM("w", 3, 3), // + // -- M MGET("r", 1), // MIGRATE("rw", 0), // @@ -140,19 +193,26 @@ public enum RedisCommand { MSET("w", 2), // MSETNX("w", 2), // MULTI("rw", 0, 0), // + // -- P PERSIST("rw", 1, 1), // PEXPIRE("rw", 2), // PEXPIREAT("rw", 2), // + PEXPIRETIME("r", 1), // + PFADD("w", 10), // + PFCOUNT("r", 1), // + PFMERGE("rw", 2), // PING("r", 0, 0), // PSETEX("w", 3), // PSUBSCRIBE("r", 1), // PTTL("r", 1, 1), // // -- Q QUIT("rw", 0, 0), // + // -- R RANDOMKEY("r", 0, 0), // - + READONLY("w", 0, 0), // + READWRITE("w", 0, 0), // RENAME("w", 2, 2), // RENAMENX("w", 2, 2), // REPLICAOF("w", 2), // @@ -161,9 +221,11 @@ public enum RedisCommand { RPOPLPUSH("rw", 2, 2), // RPUSH("rw", 2), // RPUSHX("rw", 2, 2), // + // -- S SADD("rw", 2), // SAVE("rw", 0, 0), // + SCAN("r", 1), // SCARD("r", 1, 1), // SCRIPT_EXISTS("r", 1), // SCRIPT_FLUSH("rw", 0, 0), // @@ -179,6 +241,7 @@ public enum RedisCommand { SETRANGE("rw", 3, 3), // SHUTDOWN("rw", 0), // SINTER("r", 1), // + SINTERCARD("r", 1), // SINTERSTORE("rw", 2), // SISMEMBER("r", 2), // SLAVEOF("w", 2), // @@ -186,21 +249,39 @@ public enum RedisCommand { SMEMBERS("r", 1, 1), // SMOVE("rw", 3, 3), // SORT("rw", 1), // + SORT_RO("r", 1), // SPOP("rw", 1, 1), // SRANDMEMBER("r", 1, 1), // SREM("rw", 2), // + SSCAN("r", 1), // STRLEN("r", 1, 1), // SUBSCRIBE("rw", 1), // + SUBSTR("r", 3), // SUNION("r", 1), // SUNIONSTORE("rw ", 2), // SYNC("rw", 0, 0), // + // -- T TIME("r", 0, 0), // TTL("r", 1, 1), // TYPE("r", 1, 1), // + // -- U + UNLINK("w", 1), // UNSUBSCRIBE("rw", 0), // UNWATCH("rw", 0, 0), // + + // -- V + VADD("w", 3), // + VCARD("r", 1), // + VDIM("r", 1), // + VEMB("r", 2), // + VISMEMBER("r", 2), // + VLINKS("r", 2, 3), // + VRANDMEMBER("r", 1, 2), // + VREM("w", 2), // + VSIM("w", 1), // + // -- W WATCH("rw", 1), // // -- Z @@ -220,10 +301,8 @@ public enum RedisCommand { ZREVRANK("r", 2, 2), // ZSCORE("r", 2, 2), // ZUNIONSTORE("rw", 3), // - SCAN("r", 1), // - SSCAN("r", 2), // - HSCAN("r", 2), // ZSCAN("r", 2), // + // -- UNKNOWN / DEFAULT UNKNOWN("rw", -1); diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 4ea682d900..18783d2c92 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -262,11 +262,14 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer DataType type(K key); /** - * Find all keys matching the given {@code pattern}. - * - * @param pattern must not be {@literal null}. - * @return {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: KEYS + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. + * + * @param pattern key pattern + * @return set of matching keys, or {@literal null} when used in pipeline / transaction + * @see Redis KEYS command */ @Nullable Set keys(K pattern); diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 0ef0648dea..8fcd1677e0 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Redis 3.5 GA (2025.0.0) +Spring Data Redis 3.5.4 (2025.0.4) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -57,6 +57,10 @@ conditions of the subcomponent's license, as noted in the LICENSE file. + + + + diff --git a/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java index e4d376de82..fcff1471ff 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisCommandUnitTests.java @@ -17,9 +17,10 @@ import static org.assertj.core.api.Assertions.*; -import java.util.Arrays; - import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + import org.springframework.test.util.ReflectionTestUtils; /** @@ -106,37 +107,34 @@ void shouldThrowExceptionOnInvalidArgumentCountForZaddWhenExpectedMinimalMatch() .withMessageContaining("ZADD command requires at least 3 arguments"); } - @Test // GH-2644 - void isRepresentedByIsCorrectForAllCommandsAndTheirAliases() { - - for (RedisCommand command : RedisCommand.values()) { + @ParameterizedTest(name = "{0}") // GH-2644 + @EnumSource(RedisCommand.class) + void isRepresentedByIsCorrectForAllCommandsAndTheirAliases(RedisCommand command) { - assertThat(command.isRepresentedBy(command.name())).isTrue(); - assertThat(command.isRepresentedBy(command.name().toLowerCase())).isTrue(); + assertThat(command.isRepresentedBy(command.name())).isTrue(); + assertThat(command.isRepresentedBy(command.name().toLowerCase())).isTrue(); - for (String alias : command.getAliases()) { - assertThat(command.isRepresentedBy(alias)).isTrue(); - assertThat(command.isRepresentedBy(alias.toUpperCase())).isTrue(); - } + for (String alias : command.getAliases()) { + assertThat(command.isRepresentedBy(alias)).isTrue(); + assertThat(command.isRepresentedBy(alias.toUpperCase())).isTrue(); } } - @Test // GH-2646 - void commandRequiresArgumentsIsCorrect() { + @ParameterizedTest(name = "{0}") // GH-2646 + @EnumSource(RedisCommand.class) + void commandRequiresArgumentsIsCorrect(RedisCommand command) { - Arrays.stream(RedisCommand.values()) - .forEach(command -> assertThat(command.requiresArguments()) - .describedAs("Redis command [%s] failed required arguments check", command) - .isEqualTo((int) ReflectionTestUtils.getField(command, "minArgs") > 0)); + assertThat(command.requiresArguments()).describedAs("Redis command [%s] failed required arguments check", command) + .isEqualTo((int) ReflectionTestUtils.getField(command, "minArgs") > 0); } - @Test // GH-2646 - void commandRequiresExactNumberOfArgumentsIsCorrect() { + @ParameterizedTest(name = "{0}") // GH-2646 + @EnumSource(RedisCommand.class) + void commandRequiresExactNumberOfArgumentsIsCorrect(RedisCommand command) { - Arrays.stream(RedisCommand.values()) - .forEach(command -> assertThat(command.requiresExactNumberOfArguments()) - .describedAs("Redis command [%s] failed requires exact arguments check").isEqualTo( - ReflectionTestUtils.getField(command, "minArgs") == ReflectionTestUtils.getField(command, "maxArgs"))); + assertThat(command.requiresExactNumberOfArguments()) + .describedAs("Redis command [%s] failed requires exact arguments check".formatted(command.name())).isEqualTo( + ReflectionTestUtils.getField(command, "minArgs") == ReflectionTestUtils.getField(command, "maxArgs")); } }