From 6d04aca1f132356fed76faa6f42781c0a9daa8a0 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Thu, 9 Apr 2026 14:13:57 +0300 Subject: [PATCH 1/4] GH-10782: Use CAS/CAD commands in RedisLockRegistry * Use CAS for lock renewal in RedisLock * Use CAD for lock release in RedisSpinLock * Add graceful fallback to Lua scripts for older Redis versions * Bump test container to Redis 8.4.0 * Add tests for CAS/CAD fallback and lock safety Signed-off-by: yordantsintsov --- .../redis/util/RedisLockRegistry.java | 77 ++++++++++++++-- .../integration/redis/RedisContainerTest.java | 2 +- .../redis/util/RedisLockRegistryTests.java | 89 +++++++++++++++++++ 3 files changed, 158 insertions(+), 10 deletions(-) diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java index 964019e7d5..9ba2865fae 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java @@ -48,6 +48,8 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -99,6 +101,7 @@ * @author Youbin Wu * @author Michal Domagala * @author Severin Kistler + * @author Yordan Tsintsov * * @since 4.0 * @@ -173,6 +176,8 @@ protected boolean removeEldestEntry(Entry eldest) { */ private volatile @Nullable RedisMessageListenerContainer redisMessageListenerContainer; + private volatile boolean supportsCasCadOperations = true; + /** * Create a lock registry with the default (60 second) lock expiration. * @param connectionFactory The connection factory. @@ -373,6 +378,13 @@ private Function getRedisLockConstructor(RedisLockType redisL }; } + private boolean isCasCadNotSupportedError(Exception ex) { + Throwable cause = ex.getCause(); + return cause != null + && cause.getMessage() != null + && (cause.getMessage().contains("ERR syntax error") || cause.getMessage().contains("ERR unknown command")); + } + private abstract class RedisLock implements DistributedLock { private static final String OBTAIN_LOCK_SCRIPT = """ @@ -591,13 +603,42 @@ private void removeLockKey() { } protected final boolean renew(long expireAfter) { - boolean res = Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( - RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId, String.valueOf(expireAfter))); - if (!res) { + Boolean res; + + if (RedisLockRegistry.this.supportsCasCadOperations) { + try { + res = RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).set( + RedisLockRegistry.this.clientId, + spec -> spec.ifEquals() + .value(RedisLockRegistry.this.clientId) + .expire(Duration.ofMillis(expireAfter))); + } + catch (RedisSystemException | InvalidDataAccessApiUsageException ex) { + if (isCasCadNotSupportedError(ex)) { + LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); + RedisLockRegistry.this.supportsCasCadOperations = false; + res = RedisLockRegistry.this.redisTemplate.execute( + RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId, String.valueOf(expireAfter)); + } + else { + throw ex; + } + } + } + else { + res = RedisLockRegistry.this.redisTemplate.execute( + RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId, String.valueOf(expireAfter)); + } + + boolean result = Boolean.TRUE.equals(res); + + if (!result) { stopRenew(); } - return res; + + return result; } protected final void stopRenew() { @@ -833,11 +874,29 @@ protected boolean tryRedisLockInner(long time, long expireAfter) throws Interrup @Override protected boolean removeLockKeyInnerUnlink() { - return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( - UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId)); + if (RedisLockRegistry.this.supportsCasCadOperations) { + try { + return RedisLockRegistry.this.redisTemplate.delete(this.lockKey, it -> it.ifEquals().value(RedisLockRegistry.this.clientId)); + } + catch (RedisSystemException | InvalidDataAccessApiUsageException ex) { + if (isCasCadNotSupportedError(ex)) { + LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); + RedisLockRegistry.this.supportsCasCadOperations = false; + return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( + UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId)); + } + else { + throw ex; + } + } + } + else { + return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( + UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId)); + } } - } } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/RedisContainerTest.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/RedisContainerTest.java index 4c9f274818..94ec4f8831 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/RedisContainerTest.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/RedisContainerTest.java @@ -58,7 +58,7 @@ @Testcontainers(disabledWithoutDocker = true) public interface RedisContainerTest { - GenericContainer REDIS_CONTAINER = new GenericContainer<>("redis:7.0.2") + GenericContainer REDIS_CONTAINER = new GenericContainer<>("redis:8.4.0") .withExposedPorts(6379); @BeforeAll diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java index 8dcab5da1b..1f1e1e3ffe 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java @@ -54,6 +54,7 @@ import org.springframework.integration.support.locks.DistributedLock; import org.springframework.integration.test.util.TestUtils; import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -73,6 +74,7 @@ * @author Youbin Wu * @author Glenn Renfro * @author Jiandong Ma + * @author Yordan Tsintsov * * @since 4.0 * @@ -999,6 +1001,93 @@ void testInitialiseWithCustomExecutor() { assertThatNoException().isThrownBy(() -> redisLockRegistry.setExecutor(mock())); } + @Test + void testRenewFallbackWhenCasCadNotSupported() { + RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); + registry.setRedisLockType(testRedisLockType); + + ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); + + Lock lock = registry.obtain("foo"); + assertThat(lock.tryLock()).isTrue(); + try { + registry.renewLock("foo"); + } + finally { + lock.unlock(); + } + } + + @Test + void testUnlockFallbackWhenCasCadNotSupported() { + RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); + registry.setRedisLockType(testRedisLockType); + + ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); + + Lock lock = registry.obtain("foo"); + lock.lock(); + assertThatNoException().isThrownBy(lock::unlock); + } + + @Test + void testRenewActuallyExtendsTtl() throws InterruptedException { + long shortExpiry = 200; + RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey, shortExpiry); + registry.setRedisLockType(testRedisLockType); + + Lock lock = registry.obtain("foo"); + assertThat(lock.tryLock()).isTrue(); + try { + registry.renewLock("foo", Duration.ofSeconds(10)); + Thread.sleep(shortExpiry + 100); + StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory); + assertThat(template.hasKey(this.registryKey + ":foo")).isTrue(); + } + finally { + lock.unlock(); + } + } + + @Test + void testUnlockDoesNotDeleteOtherClientsLock() throws Exception { + RedisLockRegistry registry1 = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 100); + registry1.setRedisLockType(testRedisLockType); + RedisLockRegistry registry2 = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 10000); + registry2.setRedisLockType(testRedisLockType); + + Lock lock1 = registry1.obtain("foo"); + lock1.lock(); + + waitForExpire("foo"); + + Lock lock2 = registry2.obtain("foo"); + assertThat(lock2.tryLock()).isTrue(); + try { + assertThatThrownBy(lock1::unlock).isInstanceOf(ConcurrentModificationException.class); + } + finally { + lock2.unlock(); + } + registry1.destroy(); + registry2.destroy(); + } + + @Test + void testSupportsCasCadFlagSharedAcrossLocks() { + RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); + registry.setRedisLockType(testRedisLockType); + + assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isTrue(); + + ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); + + Lock lock = registry.obtain("foo"); + lock.lock(); + assertThatNoException().isThrownBy(lock::unlock); + registry.destroy(); + } + private Long getExpire(RedisLockRegistry registry, String lockKey) { StringRedisTemplate template = createTemplate(); String registryKey = TestUtils.getPropertyValue(registry, "registryKey"); From c31eb2fda01eb3f312fea6b2d19a148f5706c3ee Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Thu, 9 Apr 2026 15:26:14 +0300 Subject: [PATCH 2/4] Updated adoc files. Signed-off-by: yordantsintsov --- src/reference/antora/modules/ROOT/pages/redis.adoc | 3 +++ src/reference/antora/modules/ROOT/pages/whats-new.adoc | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/reference/antora/modules/ROOT/pages/redis.adoc b/src/reference/antora/modules/ROOT/pages/redis.adoc index ad0503efdc..45f8b8348c 100644 --- a/src/reference/antora/modules/ROOT/pages/redis.adoc +++ b/src/reference/antora/modules/ROOT/pages/redis.adoc @@ -866,6 +866,9 @@ Starting with version 7.0, the `RedisLock` implements `DistributedLock` interfac A `RedisLock` can now be acquired using the `lock(Duration ttl)` or `tryLock(long time, TimeUnit unit, Duration ttl)` method, with a specified time-to-live (TTL) value. The `RedisLockRegistry` now provides new `renewLock(Object lockKey, Duration ttl)` method, allowing to renew the lock with a custom time-to-live value. +Starting with version 7.1, the `RedisLockRegistry` prefers native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and spin-lock release. +This requires Redis 8.4 or later. For older Redis versions, the registry automatically falls back to the previous Lua script-based approach. + [[elasticache-valkey-cluster]] === AWS ElastiCache for Valkey Support in cluster mode diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 93e0a2d186..aa95686d3d 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -53,6 +53,9 @@ See xref:testing.adoc[] for more information. The `RedisMessageStore.doRemove` now uses `GETDEL` instead of `GET` + `UNLINK` for Redis 6.2+ by default. Use `RedisMessageStore.setUseUnlink(true)` to use `GET` + `UNLINK` when atomicity is not required and `GETDEL` causes noticeable Redis latency. + +The RedisLockRegistry now uses native Redis CAS/CAD commands for lock renewal and release (Redis 8.4+), with automatic fallback to Lua scripts for older Redis versions. + See xref:redis.adoc[] for more information. [[x7.1-jms-changes]] From 2ad9b1565c96fe016e56aadbcc65619a4bfe57c5 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Wed, 15 Apr 2026 13:50:44 +0300 Subject: [PATCH 3/4] Addressed comments. Signed-off-by: yordantsintsov --- .../redis/util/RedisLockRegistry.java | 46 +++++++++------ .../redis/util/RedisLockRegistryTests.java | 59 +++++++------------ .../antora/modules/ROOT/pages/redis.adoc | 3 +- 3 files changed, 50 insertions(+), 58 deletions(-) diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java index 9ba2865fae..f723adcff1 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java @@ -53,6 +53,7 @@ import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.BoundValueOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; @@ -176,6 +177,9 @@ protected boolean removeEldestEntry(Entry eldest) { */ private volatile @Nullable RedisMessageListenerContainer redisMessageListenerContainer; + /** + * Flag to denote whether the Redis server supports CAS/CAD operations. + */ private volatile boolean supportsCasCadOperations = true; /** @@ -378,7 +382,7 @@ private Function getRedisLockConstructor(RedisLockType redisL }; } - private boolean isCasCadNotSupportedError(Exception ex) { + private static boolean isCasCadNotSupportedError(Exception ex) { Throwable cause = ex.getCause(); return cause != null && cause.getMessage() != null @@ -415,6 +419,8 @@ private abstract class RedisLock implements DistributedLock { protected final String lockKey; + protected final BoundValueOperations boundValueOps; + private final ReentrantLock localLock = new ReentrantLock(); private volatile long lockedAt; @@ -423,6 +429,7 @@ private abstract class RedisLock implements DistributedLock { private RedisLock(String path) { this.lockKey = constructLockKey(path); + this.boundValueOps = RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey); } private String constructLockKey(String path) { @@ -445,7 +452,7 @@ protected abstract boolean tryRedisLockInner(long time, long expireAfter) throws ExecutionException, InterruptedException; /** - * Unlock the lock using the unlink method in redis. + * Unlock the lock. Uses delete method for Redis 8.4 and higher or unlink for earlier versions. */ protected abstract boolean removeLockKeyInnerUnlink(); @@ -607,7 +614,7 @@ protected final boolean renew(long expireAfter) { if (RedisLockRegistry.this.supportsCasCadOperations) { try { - res = RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).set( + res = this.boundValueOps.set( RedisLockRegistry.this.clientId, spec -> spec.ifEquals() .value(RedisLockRegistry.this.clientId) @@ -615,11 +622,9 @@ protected final boolean renew(long expireAfter) { } catch (RedisSystemException | InvalidDataAccessApiUsageException ex) { if (isCasCadNotSupportedError(ex)) { - LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); + LOGGER.warn("CAS/CAD for value operations not supported, falling back to Lua script", ex); RedisLockRegistry.this.supportsCasCadOperations = false; - res = RedisLockRegistry.this.redisTemplate.execute( - RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId, String.valueOf(expireAfter)); + res = executeRenewRedisScript(expireAfter); } else { throw ex; @@ -627,9 +632,7 @@ protected final boolean renew(long expireAfter) { } } else { - res = RedisLockRegistry.this.redisTemplate.execute( - RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId, String.valueOf(expireAfter)); + res = executeRenewRedisScript(expireAfter); } boolean result = Boolean.TRUE.equals(res); @@ -641,6 +644,12 @@ protected final boolean renew(long expireAfter) { return result; } + private Boolean executeRenewRedisScript(long expireAfter) { + return RedisLockRegistry.this.redisTemplate.execute( + RENEW_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId, String.valueOf(expireAfter)); + } + protected final void stopRenew() { ScheduledFuture renewFutureToCancel = this.renewFuture; if (renewFutureToCancel != null) { @@ -880,11 +889,9 @@ protected boolean removeLockKeyInnerUnlink() { } catch (RedisSystemException | InvalidDataAccessApiUsageException ex) { if (isCasCadNotSupportedError(ex)) { - LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex); + LOGGER.warn("CAS/CAD for value operations not supported, falling back to Lua script", ex); RedisLockRegistry.this.supportsCasCadOperations = false; - return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( - UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId)); + return executeUnlinkUnlockRedisScript(); } else { throw ex; @@ -892,11 +899,16 @@ protected boolean removeLockKeyInnerUnlink() { } } else { - return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( - UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), - RedisLockRegistry.this.clientId)); + return executeUnlinkUnlockRedisScript(); } } + + private Boolean executeUnlinkUnlockRedisScript() { + return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( + UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), + RedisLockRegistry.this.clientId)); + } + } } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java index 1f1e1e3ffe..c2c334bfd6 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java @@ -35,6 +35,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -47,7 +48,10 @@ import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.BoundValueOperations; +import org.springframework.data.redis.core.SetSpec; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.integration.redis.RedisContainerTest; import org.springframework.integration.redis.util.RedisLockRegistry.RedisLockType; @@ -60,6 +64,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -1006,12 +1012,21 @@ void testRenewFallbackWhenCasCadNotSupported() { RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); registry.setRedisLockType(testRedisLockType); - ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); - Lock lock = registry.obtain("foo"); assertThat(lock.tryLock()).isTrue(); + try { + BoundValueOperations boundValueOps = mock(); + ReflectionTestUtils.setField(lock, "boundValueOps", boundValueOps); + + given(boundValueOps.set(any(), (Consumer>) any())) + .willThrow(new RedisSystemException("CAS failed", new RuntimeException("ERR unknown command"))); + + assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isTrue(); + registry.renewLock("foo"); + + assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isFalse(); } finally { lock.unlock(); @@ -1025,30 +1040,11 @@ void testUnlockFallbackWhenCasCadNotSupported() { ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); - Lock lock = registry.obtain("foo"); + Lock lock = registry.obtain("testLock"); lock.lock(); assertThatNoException().isThrownBy(lock::unlock); } - @Test - void testRenewActuallyExtendsTtl() throws InterruptedException { - long shortExpiry = 200; - RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey, shortExpiry); - registry.setRedisLockType(testRedisLockType); - - Lock lock = registry.obtain("foo"); - assertThat(lock.tryLock()).isTrue(); - try { - registry.renewLock("foo", Duration.ofSeconds(10)); - Thread.sleep(shortExpiry + 100); - StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory); - assertThat(template.hasKey(this.registryKey + ":foo")).isTrue(); - } - finally { - lock.unlock(); - } - } - @Test void testUnlockDoesNotDeleteOtherClientsLock() throws Exception { RedisLockRegistry registry1 = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 100); @@ -1068,24 +1064,9 @@ void testUnlockDoesNotDeleteOtherClientsLock() throws Exception { } finally { lock2.unlock(); + registry1.destroy(); + registry2.destroy(); } - registry1.destroy(); - registry2.destroy(); - } - - @Test - void testSupportsCasCadFlagSharedAcrossLocks() { - RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); - registry.setRedisLockType(testRedisLockType); - - assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isTrue(); - - ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false); - - Lock lock = registry.obtain("foo"); - lock.lock(); - assertThatNoException().isThrownBy(lock::unlock); - registry.destroy(); } private Long getExpire(RedisLockRegistry registry, String lockKey) { diff --git a/src/reference/antora/modules/ROOT/pages/redis.adoc b/src/reference/antora/modules/ROOT/pages/redis.adoc index 45f8b8348c..9f9b95f047 100644 --- a/src/reference/antora/modules/ROOT/pages/redis.adoc +++ b/src/reference/antora/modules/ROOT/pages/redis.adoc @@ -866,8 +866,7 @@ Starting with version 7.0, the `RedisLock` implements `DistributedLock` interfac A `RedisLock` can now be acquired using the `lock(Duration ttl)` or `tryLock(long time, TimeUnit unit, Duration ttl)` method, with a specified time-to-live (TTL) value. The `RedisLockRegistry` now provides new `renewLock(Object lockKey, Duration ttl)` method, allowing to renew the lock with a custom time-to-live value. -Starting with version 7.1, the `RedisLockRegistry` prefers native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and spin-lock release. -This requires Redis 8.4 or later. For older Redis versions, the registry automatically falls back to the previous Lua script-based approach. +Starting with version 7.1, the RedisLockRegistry uses native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands for Redis 8.4 or later. For older Redis versions, the registry automatically falls back to the previous Lua script-based approach. [[elasticache-valkey-cluster]] === AWS ElastiCache for Valkey Support in cluster mode From 3b66522eb895d6cc6d0503dd8a6d26fd21e4f045 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Thu, 16 Apr 2026 14:29:35 +0300 Subject: [PATCH 4/4] Addressed comments. Signed-off-by: yordantsintsov --- .../integration/redis/util/RedisLockRegistryTests.java | 10 +++++----- src/reference/antora/modules/ROOT/pages/redis.adoc | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java index c2c334bfd6..3430c4c56e 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java @@ -1012,7 +1012,7 @@ void testRenewFallbackWhenCasCadNotSupported() { RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey); registry.setRedisLockType(testRedisLockType); - Lock lock = registry.obtain("foo"); + Lock lock = registry.obtain("testLock"); assertThat(lock.tryLock()).isTrue(); try { @@ -1024,7 +1024,7 @@ void testRenewFallbackWhenCasCadNotSupported() { assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isTrue(); - registry.renewLock("foo"); + registry.renewLock("testLock"); assertThat(TestUtils.getPropertyValue(registry, "supportsCasCadOperations")).isFalse(); } @@ -1052,12 +1052,12 @@ void testUnlockDoesNotDeleteOtherClientsLock() throws Exception { RedisLockRegistry registry2 = new RedisLockRegistry(redisConnectionFactory, this.registryKey, 10000); registry2.setRedisLockType(testRedisLockType); - Lock lock1 = registry1.obtain("foo"); + Lock lock1 = registry1.obtain("testLock"); lock1.lock(); - waitForExpire("foo"); + waitForExpire("testLock"); - Lock lock2 = registry2.obtain("foo"); + Lock lock2 = registry2.obtain("testLock"); assertThat(lock2.tryLock()).isTrue(); try { assertThatThrownBy(lock1::unlock).isInstanceOf(ConcurrentModificationException.class); diff --git a/src/reference/antora/modules/ROOT/pages/redis.adoc b/src/reference/antora/modules/ROOT/pages/redis.adoc index ebf575845e..439bd83ba5 100644 --- a/src/reference/antora/modules/ROOT/pages/redis.adoc +++ b/src/reference/antora/modules/ROOT/pages/redis.adoc @@ -1333,7 +1333,8 @@ Starting with version 7.0, the `RedisLock` implements `DistributedLock` interfac A `RedisLock` can now be acquired using the `lock(Duration ttl)` or `tryLock(long time, TimeUnit unit, Duration ttl)` method, with a specified time-to-live (TTL) value. The `RedisLockRegistry` now provides new `renewLock(Object lockKey, Duration ttl)` method, allowing to renew the lock with a custom time-to-live value. -Starting with version 7.1, the RedisLockRegistry uses native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands for Redis 8.4 or later. For older Redis versions, the registry automatically falls back to the previous Lua script-based approach. +Starting with version 7.1, the RedisLockRegistry uses native Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands for Redis 8.4 or later. +For older Redis versions, the registry automatically falls back to the previous Lua script-based approach. [[elasticache-valkey-cluster]] === AWS ElastiCache for Valkey Support in cluster mode