Skip to content

Use CAS/CAD commands in RedisLockRegistry#10930

Open
Dgramada wants to merge 2 commits intospring-projects:mainfrom
Dgramada:topic/update-RedisLockRegistry-to-leverage-CAS/CAD
Open

Use CAS/CAD commands in RedisLockRegistry#10930
Dgramada wants to merge 2 commits intospring-projects:mainfrom
Dgramada:topic/update-RedisLockRegistry-to-leverage-CAS/CAD

Conversation

@Dgramada
Copy link
Copy Markdown
Contributor

@Dgramada Dgramada commented Apr 9, 2026

References #10782

This PR updates RedisLockRegistry to prefer Redis CAS (Compare-And-Set) and CAD (Compare-And-Delete) commands over Lua scripts for lock renewal and lock release in RedisSpinLock.

CAS/CAD are native Redis commands (since. 8.4) that provide the same conditional semantics as the equivalent Lua scripts but without the overhead of script evaluation.

Dgramada added 2 commits April 9, 2026 14:13
* 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 <yordan.tsintsov@gmail.com>
Signed-off-by: yordantsintsov <yordan.tsintsov@gmail.com>
Copy link
Copy Markdown
Contributor

@cppwfs cppwfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution!

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LUA script execution can be put into a method so that we can reduce code duplication.
Meaning we can use that method here at line 620 and 630.

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LUA script execution can be put into a method so that we can reduce code duplication.
Meaning we can use that method here at line 885 and 895.


ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false);

Lock lock = registry.obtain("foo");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:
Can we change the name of the lock from "foo" to "testLock" in the tests.

Thanks!

finally {
lock2.unlock();
}
registry1.destroy();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably want to move the registry1.destroy and registry2.destroy in a finally block to make sure they are destroyed in the case that an exception is thrown.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword this as:
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks promising.
Thank you!

};
}

private boolean isCasCadNotSupportedError(Exception ex) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static ?


if (RedisLockRegistry.this.supportsCasCadOperations) {
try {
res = RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).set(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can boundValueOps be extracted into an instance property?
The RedisLock ctor looks like a nice place for that one.

}
catch (RedisSystemException | InvalidDataAccessApiUsageException ex) {
if (isCasCadNotSupportedError(ex)) {
LOGGER.debug("CAS/CAD for value operations not supported, falling back to Lua script", ex);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warn() instead?

RedisLockRegistry.this.clientId));
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blank line still must be present.
Every class member should be surrounded with blank line to make code more readable.

RedisLockRegistry registry = new RedisLockRegistry(redisConnectionFactory, this.registryKey);
registry.setRedisLockType(testRedisLockType);

ReflectionTestUtils.setField(registry, "supportsCasCadOperations", false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like this is about falback.
With this you explicitly say that we even don't try CasCad.
Consider to spy RedisTemplate and stub those respective methods to fail with error.
Then we really would see a fallback in action.

assertThat(lock.tryLock()).isTrue();
try {
registry.renewLock("foo", Duration.ofSeconds(10));
Thread.sleep(shortExpiry + 100);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not reliable test.
The CPU could be busy with something else much longer than expire period.
It might be better to verify that TTL was extended after renew call.
But sleep and then check if it is still there is not OK.

Lock lock = registry.obtain("foo");
lock.lock();
assertThatNoException().isThrownBy(lock::unlock);
registry.destroy();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test does not have a any value.
Can we consider to remove it altogether?

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

@cppwfs cppwfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again thank you for the contribution. Just a couple more requests.

*/
private volatile @Nullable RedisMessageListenerContainer redisMessageListenerContainer;

private volatile boolean supportsCasCadOperations = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to provide a javadoc for this variable.

RedisLockRegistry.this.clientId));
if (RedisLockRegistry.this.supportsCasCadOperations) {
try {
return RedisLockRegistry.this.redisTemplate.delete(this.lockKey, it -> it.ifEquals().value(RedisLockRegistry.this.clientId));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 450 the javadocs states we do an unlink, but here we are doing a delete. The javadoc needs to be updated to state that for redis 8.4+ we do a delete else its an unlink.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants