Adds support for Redis SET … GET command.

Closes #2853
Original pull request: #3017
This commit is contained in:
Marcin Grzejszczak
2024-10-10 15:24:43 +02:00
committed by Mark Paluch
parent ea4d7280e2
commit cdf9dc360c
17 changed files with 280 additions and 0 deletions

View File

@@ -770,6 +770,11 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
return convertAndReturn(delegate.set(key, value, expiration, option), Converters.identityConverter());
}
@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return convertAndReturn(delegate.setGet(key, value, expiration, option), Converters.identityConverter());
}
@Override
public Boolean setBit(byte[] key, long offset, boolean value) {
return convertAndReturn(delegate.setBit(key, offset, value), Converters.identityConverter());

View File

@@ -326,6 +326,13 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr
return stringCommands().set(key, value, expiration, option);
}
/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
@Override
@Deprecated
default byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return stringCommands().setGet(key, value, expiration, option);
}
/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
@Override
@Deprecated

View File

@@ -47,6 +47,7 @@ import org.springframework.util.Assert;
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Marcin Grzejszczak
* @since 2.0
*/
public interface ReactiveStringCommands {
@@ -193,6 +194,41 @@ public interface ReactiveStringCommands {
*/
Flux<BooleanResponse<SetCommand>> set(Publisher<SetCommand> commands);
/**
* Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} for no expiration time or
* {@link Expiration#keepTtl()} to keep the existing.
* @param option must not be {@literal null}.
* @return
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
@Nullable
default Mono<ByteBuffer> setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
return setGet(Mono.just(SetCommand.set(key).value(value).withSetOption(option).expiring(expiration))).next()
.map(CommandResponse::getOutput);
}
/**
* Set each and every item separately by invoking {@link SetCommand}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param commands must not be {@literal null}.
* @return {@link Flux} of {@link ByteBufferResponse} holding the {@link SetCommand} along with the command result.
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
*/
Flux<ByteBufferResponse<SetCommand>> setGet(Publisher<SetCommand> commands);
/**
* Get single element stored at {@literal key}.
*

View File

@@ -122,6 +122,22 @@ public interface RedisStringCommands {
@Nullable
Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);
/**
* Set {@code value} for {@code key}. Return the old string stored at key, or nil if key did not exist.
* An error is returned and SET aborted if the value stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl or
* {@link Expiration#keepTtl()} to keep the existing expiration.
* @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing.
* @return {@literal null} when used in pipeline / transaction.
* @since 3.4
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
*/
@Nullable
byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option);
/**
* Set {@code value} for {@code key}, only if {@code key} does not exist.
*

View File

@@ -150,6 +150,24 @@ class JedisClusterStringCommands implements RedisStringCommands {
}
}
@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");
SetParams setParams = JedisConverters.toSetCommandExPxArgument(expiration,
JedisConverters.toSetCommandNxXxArgument(option));
try {
return connection.getCluster().setGet(key, value, setParams);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
@Override
public Boolean setNX(byte[] key, byte[] value) {

View File

@@ -116,6 +116,20 @@ class JedisStringCommands implements RedisStringCommands {
.getOrElse(Converters.stringToBooleanConverter(), () -> false);
}
@Override
@Nullable
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");
SetParams params = JedisConverters.toSetCommandExPxArgument(expiration,
JedisConverters.toSetCommandNxXxArgument(option));
return connection.invoke().just(Jedis::setGet, PipelineBinaryCommands::setGet, key, value, params);
}
@Override
public Boolean setNX(byte[] key, byte[] value) {

View File

@@ -105,6 +105,19 @@ class LettuceReactiveStringCommands implements ReactiveStringCommands {
}));
}
@Override
public Flux<ByteBufferResponse<SetCommand>> setGet(Publisher<SetCommand> commands) {
return this.connection.execute(reactiveCommands -> Flux.from(commands).concatMap((command) -> {
Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getValue(), "Value must not be null");
return reactiveCommands.setGet(command.getKey(), command.getValue())
.map(v -> new ByteBufferResponse<>(command, v))
.defaultIfEmpty(new AbsentByteBufferResponse<>(command));
}));
}
@Override
public Flux<ByteBufferResponse<SetCommand>> getSet(Publisher<SetCommand> commands) {

View File

@@ -115,6 +115,18 @@ class LettuceStringCommands implements RedisStringCommands {
.orElse(LettuceConverters.stringToBooleanConverter(), false);
}
@Override
@Nullable
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");
return connection.invoke()
.just(RedisStringAsyncCommands::setGet, key, value, LettuceConverters.toSetArgs(expiration, option));
}
@Override
public Boolean setNX(byte[] key, byte[] value) {

View File

@@ -49,6 +49,19 @@ public interface BoundValueOperations<K, V> extends BoundKeyOperations<K> {
*/
void set(V value, long timeout, TimeUnit unit);
/**
* Set the {@code value} and expiration {@code timeout} for the bound key. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param value must not be {@literal null}.
* @param timeout
* @param unit must not be {@literal null}.
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
V setGet(V value, long timeout, TimeUnit unit);
/**
* Set the {@code value} and expiration {@code timeout} for the bound key.
*

View File

@@ -77,6 +77,18 @@ class DefaultReactiveValueOperations<K, V> implements ReactiveValueOperations<K,
stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT));
}
@Override
public Mono<V> setGet(K key, V value, Duration timeout) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(timeout, "Duration must not be null");
return createMono(stringCommands ->
stringCommands.setGet(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT))
.map(this::readRequiredValue);
}
@Override
public Mono<Boolean> setIfAbsent(K key, V value) {

View File

@@ -208,6 +208,27 @@ class DefaultValueOperations<K, V> extends AbstractOperations<K, V> implements V
execute(connection -> connection.set(rawKey, rawValue, Expiration.from(timeout, unit), SetOption.upsert()));
}
@Override
public V setGet(K key, V value, long timeout, TimeUnit unit) {
return doSetGet(key, value, Expiration.from(timeout, unit));
}
@Override
public V setGet(K key, V value, Duration duration) {
return doSetGet(key, value, Expiration.from(duration));
}
private V doSetGet(K key, V value, Expiration duration) {
byte[] rawValue = rawValue(value);
return execute( new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.stringCommands().setGet(rawKey, rawValue, duration, SetOption.UPSERT);
}
});
}
@Override
public Boolean setIfAbsent(K key, V value) {

View File

@@ -58,6 +58,18 @@ public interface ReactiveValueOperations<K, V> {
*/
Mono<Boolean> set(K key, V value, Duration timeout);
/**
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value
* @param timeout must not be {@literal null}.
* @see <a href="https://redis.io/commands/setex">Redis Documentation: SETEX</a>
*/
Mono<V> setGet(K key, V value, Duration timeout);
/**
* Set {@code key} to hold the string {@code value} if {@code key} is absent.
*

View File

@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Mark Paluch
* @author Jiahe Cai
* @author Marcin Grzejszczak
*/
public interface ValueOperations<K, V> {
@@ -44,6 +45,33 @@ public interface ValueOperations<K, V> {
*/
void set(K key, V value);
/**
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param timeout the key expiration timeout.
* @param unit must not be {@literal null}.
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
V setGet(K key, V value, long timeout, TimeUnit unit);
/**
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param duration expiration duration
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
V setGet(K key, V value, Duration duration);
/**
* Set the {@code value} and expiration {@code timeout} for {@code key}.
*

View File

@@ -1093,6 +1093,11 @@ class RedisConnectionUnitTests {
return delegate.set(key, value, expiration, options);
}
@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return delegate.setGet(key, value, expiration, option);
}
@Override
public List<Long> bitField(byte[] key, BitFieldSubCommands subCommands) {
return delegate.bitField(key, subCommands);

View File

@@ -556,4 +556,29 @@ public class LettuceReactiveStringCommandsIntegrationTests extends LettuceReacti
assertThat(nativeBinaryCommands.ttl(KEY_1_BBUFFER)).isCloseTo(expireSeconds, Offset.offset(5L));
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
}
@ParameterizedRedisTest // GH-2853
void setGetMono() {
nativeCommands.set(KEY_1, VALUE_1);
connection.stringCommands().setGet(KEY_1_BBUFFER, VALUE_2_BBUFFER, Expiration.keepTtl(), SetOption.upsert())
.as(StepVerifier::create) //
.expectNext(VALUE_1_BBUFFER) //
.verifyComplete();
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
}
@ParameterizedRedisTest // GH-2853
void setGetFlux() {
nativeCommands.set(KEY_1, VALUE_1);
connection.stringCommands().setGet(Mono.just(SetCommand.set(KEY_1_BBUFFER).value(VALUE_2_BBUFFER).expiring(Expiration.keepTtl()).withSetOption( SetOption.upsert())))
.map(CommandResponse::getOutput)
.as(StepVerifier::create) //
.expectNext(VALUE_1_BBUFFER) //
.verifyComplete();
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
}
}

View File

@@ -316,6 +316,32 @@ public class DefaultValueOperationsIntegrationTests<K, V> {
await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key));
}
@ParameterizedRedisTest
void testSetGetWithExpiration() {
K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();
valueOps.set(key, value1);
assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1);
assertThat(valueOps.get(key)).isEqualTo(value2);
}
@ParameterizedRedisTest
void testSetGetWithExpirationDuration() {
K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();
valueOps.set(key, value1);
assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1);
assertThat(valueOps.get(key)).isEqualTo(value2);
}
@ParameterizedRedisTest
void testAppend() {

View File

@@ -571,6 +571,23 @@ public class RedisTemplateIntegrationTests<K, V> {
assertThat(ttl).isLessThan(25L);
}
@ParameterizedRedisTest // GH-3017
void testSetGetExpireMillis() {
K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();
redisTemplate.boundValueOps(key).set(value1);
V oldValue = redisTemplate.boundValueOps(key).setGet(value2, 1, TimeUnit.DAYS);
redisTemplate.expire(key, 1, TimeUnit.DAYS);
Long ttl = redisTemplate.getExpire(key, TimeUnit.HOURS);
assertThat(oldValue).isEqualTo(value1);
assertThat(ttl).isGreaterThanOrEqualTo(23L);
assertThat(ttl).isLessThan(25L);
}
@ParameterizedRedisTest // DATAREDIS-611
void testGetExpireDuration() {