본문 바로가기
관리자

Programming-[Backend]/Redis, Memcached

분산 락

728x90
반응형

 

멀티 인스턴스 환경일 때 특정 key값에 대해서 동시성(동시 수정) 방지를 위해 사용한다.

 

Redis의 싱글 스레드라는 특성, Redisson 라이브러리를 이용한다.

Hash 자료구조를 사용한다.

- Key: lock:{key이름}:{보통 사용하는 id값}

- Field: {Redisson UUID} : {Java Thread ID}

소유자 식별자로 사용하며, Redisson UUID를 사용하면 RedissonClient 인스턴스가 서버(JVM) 인스턴스마다 고유하게 부여한다.

ex) a3f2e1d0-b7c4-4f2a-9e88-1234abcd5678 : 52

- Value: 재진입 횟수로 정수값으로 기록한다.

 

키 및 로직

HEXIST로 내가 소유한 락인지 확인한다. HINCRBY로 원자적으로 증가시킨다. Lua Script를 통해서 다음과 같이 구현한다.

 

- key 구성: EVALSHA <script> 2 <KEYS[1]> <KEYS[2]> <ARGV[1]> <ARGV[2]> <ARGV[3]>

- 값 치환:

KEYS[1]: 실제 key 이름

KEYS[2]: pub/sub 채널명. ex) redisson_lock__channel:{lock key 값}

ARGV[1]: 소유자. UUID:threadId

ARGV[2]: leaseTime

ARGV[3]: pub 메시지. ex) 0 -> 0을 전송하여 이 field가 Hash에서 lock이 안잡혀 있음을 알림.

-- 1. 내가 소유한 락인지 확인
  if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
      return nil  -- 내 락이 아니면 무시
  end

  -- 2. 재진입 카운트 감소
  local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)

  -- 3. 아직 카운트가 남아있으면 TTL만 갱신
  if (counter > 0) then
      redis.call('pexpire', KEYS[1], ARGV[2])
      return 0
  end

  -- 4. 카운트가 0이면 키 삭제 + 대기 중인 스레드에 알림
  redis.call('del', KEYS[1])
  redis.call('publish', KEYS[2], ARGV[3])
  return 1

 

재진입 처리(reentrant)

코드가 길어지면 로직 내부에서 키 값을 중첩으로 호출할 수 있기 때문에, 같은 로직 내에서는 재진입이 가능하도록 방어적 설계로 처리함.

항상 lock과 해제가 쌍으로 돌아가야 정상적으로 0으로 돌아가며, 내부 unlock을 빠뜨리면 leaseTime동안 다른 스레드가 대기하게 된다.

lock  → count: 0 → 1 (외부 획득)
lock  → count: 1 → 2 (내부 재진입)
unlock → count: 2 → 1 (내부 해제, 키 유지)
unlock → count: 1 → 0 (외부 해제, 키 삭제 → 다른 스레드 획득 가능)
728x90
반응형