기본 Lock 동작 원리 in redis

  • A 키에 대해 락을 걸려고하면 A 키를 기반으로 락 키를 생성해준다. 예를 들어 item1이라는 키가 있으면 lock:item1와 같이 락 키를 설정해준다.
  • 이 때 nx 옵션을 주어 해당 키에 값이 없을 때만 키를 설정하도록 한다. 즉, 값이 설정되면 락을 얻었음을 의미한다. 값을 설정하기 위해서 다시 시도할 횟수와 재시도 대기 시간을 설정한다.
  • 락을 얻어 모든 연산을 수행했으면 try-finally 구문을 통해 락을 반납한다.
  • 예기치 못한 동작으로 인해 키가 영영 반환이 안 되는 경우를 대비해 px옵션을 주어 만료 시간을 설정한다.
  • 만료 시간을 설정함으로써 발생하는 문제가 있다.
    1. 락 키를 얻고 연산을 수행할 때 이 연산이 오래 걸려 연산 수행 중에 키가 만료되는 경우
      • 정상적인 상황에서 연산이 수행할게 남았다면, 만료 시간을 늘리는 방법이 있다.
    2. 연산을 모두 수행하고 delete 명령어를 수행하기 직전에 키가 만료되어 락 키에 다른 값이 설정되어(락을 대기 중인 클라이언트) delete 명령어가 새롭게 설정된 값을 제거하는 경우
      • delete를 실행하기 전에 락 키의 값을 가져온 후 해당 값이 delete를 실행하는 클라이언트가 설정한 값이랑 같은지 비교하고 같으면 delete를 수행하는 방법이 있다.
      • 하지만 이 때도 원자적으로 수행이 안 되면 결국 같은 일이 발생하므로 Lua script를 이용하여 원자적으로 동작하도록 설정한다. ( Lua script의 모든 연산이 끝날 때까지 redis는 다른 명령어를 실행하지 않는다.)

스프링 적용

  • 스프링에서는 Redisson을 이용하여 위에서 설명한 동작 원리를 쉽게 적용할 수 있다.
  • 먼저 락을 적용하는 여러 클라이언트들이 존재하지만 redisson을 고른 이유는 지속적으로 락을 획득하기 위해 reids 서버에 요청을 보내는 lettuce보다는 pub/sub 방식으로 동작하여 락 획득 실패시 특정 채널을 구독하여 락을 획득할 수 있다는 이벤트를 받았을 때 l락 획득 요청을 보내는 redisson이 레디스 서버에 부하를 덜 주기 때문이다.

RedissonConfig

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}
  • config.setLockWatchdogTimeout(millisec)을 통해 watchdog의 연장 시간을 바꿀 수 있다.
  • watchdog는 비정상적인 상황이 아니면 락을 잡는 시간을 연장시켜주는 역할을 한다. 이를 통해 위의 1번의 문제 상황을 해결할 수 있다.

 

RedissonAOP

@Aspect
@Component
@RequiredArgsConstructor
public class RedissonAOP {
    private final RedissonClient redisson;

    @Around("bean(redisService)")
    public void locking(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String lockName = args[0].toString()+":"+args[1].toString();
        RLock lock = redisson.getLock(lockName);

        try{
            boolean lockable = lock.tryLock(1000, TimeUnit.MILLISECONDS);
            if(!lockable){
                System.out.println("lock fail");
                return;
            }
            System.out.println("lock ok");
            joinPoint.proceed();
        }catch (InterruptedException e){
            System.out.println("lock interrupted");
            e.printStackTrace();
        } finally {
            System.out.println("lock end");
            lock.unlock();
        }
    }
}
  • getLock에 인자로 준 값을 통해 락키를 생성한다. 이 때 키값은 UUID로 설정된다.
  • tryLock에 첫 번째 인자는 락을 획득하기 위한 최대 대기 시간이다. 즉, 설정된 시간만큼만 락을 획득하려고 한다.
  • TimeUnit이 아닌 두 번째 인자를 줄 수 있는데 이는 자동으로 락을 해제하는 시간이다. 데드락으로 인해 락을 계속적으로 잡는 상황을 대비한 인자로, 만약 이 인자를 설정하지 않으면 기본 30초로 설정되며 watchdog가 설정된다.
  • 위의 2번 문제 상황은 Redisson에서 기본적으로 키값을 비교하여 자기가 설정한 키값이 맞는지 확인하도록 설정되어 있다.

 

RedisService

@Service
@RequiredArgsConstructor
public class RedisService {
    private final StringRedisTemplate stringRedisTemplate;

    public void increment(String prefix, int id){
        String s = stringRedisTemplate.opsForValue().get(prefix + id);
        stringRedisTemplate.opsForValue().set(prefix+id, Integer.toString(Integer.parseInt(s)+1));
    }

    public void set(String prefix, int id){
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        stringRedisTemplate.opsForValue().set(prefix + id, Integer.toString(10));
    }
}

'Database > Redis' 카테고리의 다른 글

메모리 정책 in redis  (0) 2024.11.05
Lua Script in redis  (0) 2024.11.05
List 타입 명령어  (0) 2024.10.09
HyperLogsLogs 타입 명령어  (0) 2024.10.09
관계형 데이터 사용 in redis(feat. SORT)  (0) 2024.10.07

+ Recent posts