GH-8699: Atomic Redis script for unlock()

Expected Behavior

Using a single Lua script to verify ownership of the lock and remove it.

Current Behavior

`unlock()` method of `RedisLock` uses two separate Redis operations:

    * `isAcquiredInThisProcess()`` method executes a `GET` operation to verify if the lock is owned by the process.
    * `removeLockKey()`` method executes `UNLINK/DEL` operation to remove the lock.

* The `removeLockKeyInnerUnlink()`, and `removeLockKeyInnerDelete()`
methods will execute a script both verify ownership of the lock and remove it.

**Cherry-pick to `6.1.x` & `6.0.x`**
This commit is contained in:
EddieChoCho
2023-08-12 09:47:28 -04:00
committed by Artem Bilan
parent 92fb0d3bc8
commit 0450a323ec

View File

@@ -83,6 +83,7 @@ import org.springframework.util.ReflectionUtils;
* @author Unseok Kim
* @author Anton Gabov
* @author Christian Tzolov
* @author Eddie Cho
*
* @since 4.0
*
@@ -98,7 +99,7 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
private final Lock lock = new ReentrantLock();
private final Map<String, RedisLock> locks =
new LinkedHashMap<String, RedisLock>(16, 0.75F, true) {
new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Entry<String, RedisLock> eldest) {
@@ -343,12 +344,12 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
/**
* Unlock the lock using the unlink method in redis.
*/
protected abstract void removeLockKeyInnerUnlink();
protected abstract boolean removeLockKeyInnerUnlink();
/**
* Unlock the lock using the delete method in redis.
*/
protected abstract void removeLockKeyInnerDelete();
protected abstract boolean removeLockKeyInnerDelete();
@Override
public final void lock() {
@@ -454,11 +455,6 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
return;
}
try {
if (!isAcquiredInThisProcess()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
if (Thread.currentThread().isInterrupted()) {
RedisLockRegistry.this.executor.execute(this::removeLockKey);
}
@@ -481,7 +477,11 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
private void removeLockKey() {
if (RedisLockRegistry.this.unlinkAvailable) {
try {
removeLockKeyInnerUnlink();
boolean unlinkResult = removeLockKeyInnerUnlink();
if (!unlinkResult) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
return;
}
catch (Exception ex) {
@@ -496,7 +496,10 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
}
}
}
removeLockKeyInnerDelete();
if (!removeLockKeyInnerDelete()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
}
@Override
@@ -559,19 +562,23 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
private final class RedisPubSubLock extends RedisLock {
private static final String UNLINK_UNLOCK_SCRIPT =
"if (redis.call('unlink', KEYS[1]) == 1) then " +
"redis.call('publish', ARGV[1], KEYS[1]) " +
"return true " +
"end " +
"return false";
private static final String UNLINK_UNLOCK_SCRIPT = """
local lockClientId = redis.call('GET', KEYS[1])
if (lockClientId == ARGV[1] and redis.call('UNLINK', KEYS[1]) == 1) then
redis.call('PUBLISH', ARGV[2], KEYS[1])
return true
end
return false
""";
private static final String DELETE_UNLOCK_SCRIPT =
"if (redis.call('del', KEYS[1]) == 1) then " +
"redis.call('publish', ARGV[1], KEYS[1]) " +
"return true " +
"end " +
"return false";
private static final String DELETE_UNLOCK_SCRIPT = """
local lockClientId = redis.call('GET', KEYS[1])
if (lockClientId == ARGV[1] and redis.call('DEL', KEYS[1]) == 1) then
redis.call('PUBLISH', ARGV[2], KEYS[1])
return true
end
return false
""";
private static final RedisScript<Boolean>
UNLINK_UNLOCK_REDIS_SCRIPT = new DefaultRedisScript<>(UNLINK_UNLOCK_SCRIPT, Boolean.class);
@@ -589,18 +596,19 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
}
@Override
protected void removeLockKeyInnerUnlink() {
RedisLockRegistry.this.redisTemplate.execute(
UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey),
RedisLockRegistry.this.unLockChannelKey);
protected boolean removeLockKeyInnerUnlink() {
return removeLockKeyWithScript(UNLINK_UNLOCK_REDIS_SCRIPT);
}
@Override
protected void removeLockKeyInnerDelete() {
RedisLockRegistry.this.redisTemplate.execute(
DELETE_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey),
RedisLockRegistry.this.unLockChannelKey);
protected boolean removeLockKeyInnerDelete() {
return removeLockKeyWithScript(DELETE_UNLOCK_REDIS_SCRIPT);
}
private boolean removeLockKeyWithScript(RedisScript<Boolean> redisScript) {
return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute(
redisScript, Collections.singletonList(this.lockKey),
RedisLockRegistry.this.clientId, RedisLockRegistry.this.unLockChannelKey));
}
private boolean subscribeLock(long time) throws ExecutionException, InterruptedException {
@@ -694,6 +702,30 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
private final class RedisSpinLock extends RedisLock {
private static final String UNLINK_UNLOCK_SCRIPT = """
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('UNLINK', KEYS[1])
return true
end
return false
""";
private static final String DELETE_UNLOCK_SCRIPT = """
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('DEL', KEYS[1])
return true
end
return false
""";
private static final RedisScript<Boolean>
UNLINK_UNLOCK_REDIS_SCRIPT = new DefaultRedisScript<>(UNLINK_UNLOCK_SCRIPT, Boolean.class);
private static final RedisScript<Boolean>
DELETE_UNLOCK_REDIS_SCRIPT = new DefaultRedisScript<>(DELETE_UNLOCK_SCRIPT, Boolean.class);
private RedisSpinLock(String path) {
super(path);
}
@@ -718,13 +750,19 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl
}
@Override
protected void removeLockKeyInnerUnlink() {
RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);
protected boolean removeLockKeyInnerUnlink() {
return removeLockKeyWithScript(UNLINK_UNLOCK_REDIS_SCRIPT);
}
@Override
protected void removeLockKeyInnerDelete() {
RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
protected boolean removeLockKeyInnerDelete() {
return removeLockKeyWithScript(DELETE_UNLOCK_REDIS_SCRIPT);
}
private boolean removeLockKeyWithScript(RedisScript<Boolean> redisScript) {
return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute(
redisScript, Collections.singletonList(this.lockKey),
RedisLockRegistry.this.clientId));
}
}