Upgrade to Pro — share decks privately, control downloads, hide ads and more …

BeJUG_2024_CodeDive_SpringDistributedLock_Redis...

Avatar for Eddie Cho Eddie Cho
January 25, 2024
0

 BeJUG_2024_CodeDive_SpringDistributedLock_Redis_.pdf

Avatar for Eddie Cho

Eddie Cho

January 25, 2024

Transcript

  1. 2

  2. 3 Background: Concurrency Issue with Automation end-to-end Test Processes •

    Run multiple test process simultaneously • Limited test accounts Feature file 1: account1, account2, ... Feature file 2: account1, account3, ... Feature file 3: account2, account3, ... ... Feature file 8: account4, account5, ... Feature file 9: account3, account4, ... Feature file 10: account2, account5, ... ... Process 2 ... Process 1
  3. 4 Distributed Lock: Concurrency Control with Multiple Processes Process 1

    Process 2 Lock Status acquire Critical Section ok time acquire fail acquire fail release ok acquire ok
  4. 7 How to use Spring Distributed Lock? final Lock lock

    = lockRegistry.obtain(“key”); try { if (lock.tryLock(10, TimeUnit.MINUTES)) { // do something } } catch (Exception e) { // log issue } finally { lock.unlock(); } Reference: How To Implement a Spring Distributed Lock
  5. 8 The Construction of RedisLockRegistry Bean(6.1.3) public final class RedisLockRegistry

    { //ID of the process private final String clientId = UUID.randomUUID().toString(); private final String registryKey; //prefix of the name of each key private final StringRedisTemplate redisTemplate; private final long expireAfter;.//TTL of each key public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) { this.redisTemplate = new StringRedisTemplate(connectionFactory); this.registryKey = registryKey; this.expireAfter = expireAfter; //... } }
  6. 9 Obtaining Locks from RedisLockRegistry(6.1.3) final Lock lock = lockRegistry.obtain(“key”);

    try { if (lock.tryLock(10, TimeUnit.MINUTES)) { // do something } } catch (Exception e) { // log issue } finally { lock.unlock(); } Reference: How To Implement a Spring Distributed Lock
  7. 10 Obtaining Locks from RedisLockRegistry(6.1.3) public final class RedisLockRegistry {

    private RedisLockType redisLockType = RedisLockType.SPIN_LOCK; private final Lock lock = new ReentrantLock(); private final Map<String, RedisLock> locks = new LinkedHashMap<>(); @Override public Lock obtain(Object lockKey) { String path = (String) lockKey; this.lock.lock(); try { return this.locks.computeIfAbsent( path,getRedisLockConstructor(this.redisLockType)); } finally { this.lock.unlock(); } } private Function<String, RedisLock> getRedisLockConstructor( RedisLockRegistry.RedisLockType redisLockType) { return switch (redisLockType) { case SPIN_LOCK -> RedisLockRegistry.RedisSpinLock::new; case PUB_SUB_LOCK -> RedisLockRegistry.RedisPubSubLock::new; }; }
  8. 11 Obtaining Locks from RedisLockRegistry(6.1.3) public final class RedisLockRegistry {

    private final String registryKey; private abstract class RedisLock implements Lock { protected final String lockKey; private RedisLock(String path) { // e.g., registryKey: “spring” // path: “integration” // lockKey = “spring:integration” this.lockKey = constructLockKey(path); } private String constructLockKey(String path) { return RedisLockRegistry.this.registryKey + ':' + path; } } }
  9. 12 Acquiring a Distributed Lock by Invoking tryLock Method of

    RedisLock final Lock lock = lockRegistry.obtain(“key”); try { if (lock.tryLock(10, TimeUnit.MINUTES)) { // do something } } catch (Exception e) { // log issue } finally { lock.unlock(); } Reference: How To Implement a Spring Distributed Lock
  10. 13 Acquiring a Distributed Lock by Invoking tryLock Method of

    RedisLock(6.1.3) private abstract class RedisLock implements Lock { private final ReentrantLock localLock = new ReentrantLock(); @Override public final boolean tryLock(long time, TimeUnit unit) throws InterruptedException { if (!this.localLock.tryLock(time, unit)) { return false; } try { long waitTime = TimeUnit.MILLISECONDS.convert(time, unit); // Try to insert/update the status of lock into Redis boolean acquired = tryRedisLock(waitTime); if (!acquired) { this.localLock.unlock(); } return acquired; } catch (Exception e) { this.localLock.unlock(); rethrowAsLockException(e); } return false; } }
  11. 14 Acquiring a Distributed Lock by Invoking tryLock Method of

    RedisLock(6.1.3) public final class RedisSpinLock extends RedisLock { @Override protected boolean tryRedisLockInner(long time) throws InterruptedException { long now = System.currentTimeMillis(); if (time == -1L) { while (!obtainLock()) { Thread.sleep(100); } return true; } else { long expire = now + TimeUnit.MILLISECONDS.convert(time, TimeUnit.MILLISECONDS); boolean acquired; while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire){ Thread.sleep(100); } return acquired; } } }
  12. 15 Acquiring a Distributed Lock by Invoking tryLock Method of

    RedisLock(6.1.3) private abstract class RedisLock implements Lock { protected final Boolean obtainLock() { return RedisLockRegistry.this.redisTemplate.execute( OBTAIN_LOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey),RedisLockRegistry.this.clientId, String.valueOf(RedisLockRegistry.this.expireAfter)); } // KEYS[1]: keyName (this.lockKey) // ARGV[1]: processId (RedisLockRegistry.this.clientId) // ARGV[2]: TTL (RedisLockRegistry.this.expireAfter) private static final String OBTAIN_LOCK_SCRIPT = """ local lockClientId = redis.call('GET', KEYS[1]) if lockClientId == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return true elseif not lockClientId then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return true end return false"""; protected static final RedisScript<Boolean> OBTAIN_LOCK_REDIS_SCRIPT = … ; }
  13. 16 Acquiring a Distributed Lock by Invoking tryLock Method of

    RedisLock(6.1.3) Reference: Redis – Scripting with Lua
  14. 17 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock final Lock lock = lockRegistry.obtain(“key”); try { if (lock.tryLock(10, TimeUnit.MINUTES)) { // do something } } catch (Exception e) { // log issue } finally { lock.unlock(); } Reference: How To Implement a Spring Distributed Lock
  15. 18 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock(6.1.3) private abstract class RedisLock implements Lock { private final ReentrantLock localLock = new ReentrantLock(); @Override public final void unlock() { if (!this.localLock.isHeldByCurrentThread()) { throw new IllegalStateException(); } if (this.localLock.getHoldCount() > 1) { this.localLock.unlock(); return; } try { removeLockKey(); // UNLINK/DEL to remove the ownership } catch (Exception e) { ReflectionUtils.rethrowRuntimeException(e); } finally { this.localLock.unlock(); } }}
  16. 19 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock(6.1.3) private abstract class RedisLock implements Lock { private void removeLockKey() { if(RedisLockRegistry.this.unlinkAvailable) { Boolean unlinkResult = null; try { unlinkResult = removeLockKeyInnerUnlink(); } catch (Exception ex) { RedisLockRegistry.this.unlinkAvailable = false; //... } if (Boolean.TRUE.equals(unlinkResult)) { return; } else if(Boolean.FALSE.equals(unlinkResult)) { throw new IllegalStateException(); } } if (!removeLockKeyInnerDelete()) { throw new IllegalStateException(); } }}
  17. 20 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock(6.1.3) protected boolean removeLockKeyInnerUnlink() { return Boolean.TRUE.equals(RedisLockRegistry.this.redisTemplate.execute( UNLINK_UNLOCK_REDIS_SCRIPT, Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId)); } // KEYS[1]: keyName (this.lockKey) // ARGV[1]: processId (RedisLockRegistry.this.clientId) 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 RedisScript<Boolean> UNLINK_UNLOCK_REDIS_SCRIPT = new DefaultRedisScript<>(UNLINK_UNLOCK_SCRIPT, Boolean.class); protected boolean removeLockKeyInnerDelete() { //... }
  18. 21 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock(6.1.3) Process 1 Process 2 Redis Node lock:key expired execute unlock script ok time fail Lock status: owned by process2 acquire Lock status: owned by process1
  19. 22 Releasing a Distributed Lock by Invoking the unlock Method

    of RedisLock(6.1.3) Process 1 Process 2 Redis Node lock:key expired execute unlock script ok time fail acquire ok Lock status: owned by process1 Lock status: owned by process2 Lock status: empty execute unlock script
  20. 23 Summary Spring Distributed Lock(Redis) has consider: 1) ReentrantLock: Concurrency

    control for threads in a process 2) Redis Node: keep the status of locks 3) Atomic execution: acquire/release locks 4) TTL: Deadlock prevention 5) Redis versions that do not support UNLINK Spring Distributed Lock(Redis) has not(yet) consider: 1) Custom TTL per lock(Issue #3444) 2) Supporting Redis Cluster(Redlock Algorithm)