Lock 방식과 Lock 획득을 위한 여러가지 방식

Lock(잠금)은 데이터 처리를 위해 동시성을 제어하기 위한 메커니즘으로 여러방면에서 이를 활용한다. 이 포스트에서는 어플리케이션이 취할 수 있는 Lock의 종류와 Lock으로 인해 생기는 문제점과 해결방법에 대해 작성한다.

여러가지 Lock 방식

Optimistic Lock(낙관적 락)

실제로 데이터에 Lock을 걸지는 않는다. 데이터 자체에 version을 가지고 이후 업데이트 시 처음 읽은 데이터의 version이 동일한지 확인하고 업데이트하는 방식이다.

다시말해, Compare-and-Set 방식으로 쿼리를 요청하는 방식이다.

%% 결과값의 version 3 %%
SELECT * From student WHERE student_id = 1; 

UPDATE student SET name = 'new name'
	WHERE version = 3 AND student_id = 1;

만약 version이 맞지 않다면 쿼리가 실패하여 어플리케이션 수준에서 처음부터 재시도하거나 abort 시켜야 한다. 충돌이 빈번하게 일어난다면 계속 재시도 하므로 성능이 좋지 못하다. 따라서, 충돌 자주 일어나는 작업이라면 낙관적락을 선택은 다시 한 번 생각해보는게 좋다.

Pessimistic Lock(비관적 락)

데이터베이스의 락을 사용하는 방식이다. 데이터베이스가 제공하는 락을 사용하면 좋은 점은 Lock 획득에 실패했을때 처리에 대한 로직을 어플리케이션에서 작성할 필요가 없다.(블로킹 방식으로 데이터베이스 내부에서 Lock을 획득할때까지 대기한다. Lock 획득을 위한 방식에서 이어서 이야기 한다.)

Pessimistic Lock은 데이터베이스의 하나의 로우나 테이블 단위로 Lock을 거는 방식이다. Select ~ For Udpate 구문을 이용해 쿼리를 요청함으로써 해당 로우에 대한 Lock을 명시적으로 획득한다.

SELECT * 
From student 
WHERE student_id = 1 
FOR UPDATE:

Named Lock

MetaData로 Lock을 거는 방식이다. 데이터에 Lock을 거는 것이 아닌 별도의 공간에 있는 Lock을 사용한다. Lock을 명시적으로 획득하기 위한 로직이 있어야 하며, 트랜잭션 종료시 자동으로 락이 해제되지 않아 해제를 직접 수행하거나, 선점시간이 끝나야 해제된다. Lock을 해제할 때까지 다른 세션은 Lock을 획득할 수 없다. 주로 분산락 구현시 사용한다.(분산락에서 더 설명한다)

원자적 쓰기 연산

데이터의 동시 접근을 제어하기 위한 방식이다. 실질적으로 어플리케이션에서 Lock 방식을 사용하는 것은 아니다. 보통 동시성 문제는 read-modify-write 주기 사이에서 일어난다. 대부분의 데이터베이스에서는 이 주기를 하나의Update 쿼리를 통해 원자적으로 쓸 수 있도록 제공해준다.

UPDATE counters SET value = value + 1 WHERE key = 'v';

이 쿼리 연산을 사용하면 데이터베이스가 내부에서 Exclusive Lock을 걸고 연산을 처리해준다. 하지만 쿼리에서 볼 수 있듯이 사용할 수 있는 상황이 제한적인데, 이전 데이터가 이후 데이터와 연관관계가 있을 경우에만 사용할 수 있다.

Distributed Lock(분산 락)

분산 락의 정의는 분산 환경에서 데이터의 동시성을 제어하기 위한 방법이다. 여기서 분산환경이란 여러가지 의미로 해석될 수 있다.

  • 분산 데이터베이스 환경
  • 하나의 데이터베이스라도 여러 데이터에 접근해야 하는 상황(데이터가 분산)
  • 스케일 아웃된 서버 환경(이러한 환경에서는 낙관적락이나 비관적락을 사용해서 동시성을 제어할 수 있다.)

Distributed LockNamed Lock을 이용해 구현한다. Distributed Lock은 보통 하나의 로우를 수정하면서 생기는 문제가 아니라서 데이터 수준의 잠금을 이용하는 Pessimistic Lock이나 Optimisitic Lock으로 구현하기는 힘들다.

앞서 설명한 Named Lock의 특성처럼 Distributed Lock은 명시적으로 Lock을 획득하고 직접 Lock을 해제해야한다. 때문에 Lock을 획득하기 위해 대기하는 방식에 따라 성능이 달라지기도 한다.(이후 Lock을 획득하는 방식에서 이에 대해 설명한다.)

그렇다면 언제 Distributed Lock을 사용하는 것이 좋을까? 정확히 언제 사용하는 것이 좋다라고 말하기는 어렵지만 필자가 생각하는 Distributed Lock이 필요한 상황은 다음과 같다.

  • 한 트랜잭션 내 서로다른 다수의 데이터에 락이 필요한 경우, 논리적인 하나의 Lock을 사용하면 Lock 경쟁을 최소화할 수 있고 시간적으로도 효율적이다.(한 트랜잭션에서 A,B,C의 락이 필요한데 A락을 획득 후 B락 시도시, 이미 다른 쓰레드가 B를 획득하여 데드락인 상태 발생가능이때 모든 락을 하나의 락으로 집중화시켜 데드락 문제 해결이 가능)
  • 한 집합 내 서로 다른 데이터의 수정, 추가, 삭제로 인해 한 집합의 결과가 바뀔 수 있는 경우(즉, 데이터 수준의 락으로 처리 불가능 할 경우)
  • 파티셔닝된 분산 데이터베이스 시스템에서 여러 데이터베이스에 락을 걸어야 하는 경우(첫 번째와 사례와 비슷하지만 트랜잭션이 분산 트랜잭션이란게 다름)
  • 동시에 여러 트랜잭션을 사용하는 경우, 보통 데이터 레벨의 락은 트랜잭션 종료 시 해제되므로 Named Lock을 이용한 분산락 사용

Lock을 획득을 위한 방식

만약 클라이언트의 어떤 요청에서 내에서 Lock을 획득하기 위한 과정 중 누군가 이미 Lock을 점유하고 있는 상태라면 어떡해야할까? 클라이언트에게 처리할 수 없다는 응답을 보내야할까? 그렇다면 아마도 해당 클라이언트는 원하는 요청을 영원히 처리하지 못할 수도 있다. 이러한 상황에서 이미 점유된 상태의 Lock을 획득하기 위한 몇 방식들이 존재한다.

Blocking

DB에 Lock 획득요청 후 DB에서 Lock을 획득할 때까지 대기하는 방식이다. DB 커넥션을 계속 점유하고 있기 때문에 어플리케이션 수준에서 효율성이 좋지 않다. Pessimistic Lock을 사용한다면 Blocking 방식으로 대기하게 된다. 물론 Lock 대기 및 획득을 위한 추가적인 작업이 필요없다는 장점이 존재한다.

매번 영원히 대기할 수는 없기 때문에 Select For Update 구문에 몇가지 속성들을 제공한다.

  • Select ~ FOR UPDATE no wait - Lock 획득에 실패하면 바로 포기
  • Select ~ FOR UPDATE wait 10; - Blocking 방식이 아니라 10초 동안 재시도하는 방식

Spin Lock

Lock 획득에 실패한다면 Lock을 획득할때까지 계속해서 요청을 보내는 방식이다. Lock 경합이 많이 벌어지지 않는 상황에서는 효율적일지 모르나 Lock 경합이 많이 벌어지는 상황에서 요청이 불필요하게 너무 많아질 수도 있다.

Sleep Lock

Lock 요청을 보내고 획득에 실패한다면 지정된 시간동안 대기 후 재요청을 하는 방식이다. 타이밍이 좋지 않다면 요청을 Lock을 영원히 획득하지 못할 수도 있어 적절한 시점에 timeout이 필요하다.

Pub/Sub 기반 Lock(Event 기반)

채널을 하나 만들고, Lock을 점유중인 쓰레드가 Lock 획득을 대기중인 쓰레드에게 해제를 알려주고 해당 안내를 받은 쓰레드가 Lock 획득 시도를 하는 방식이다. retry 가 필요하지 않고 데이터베이스에서 이벤트가 도착했을때 획득 요청을 보내면 된다.

참고

카테고리:

업데이트:

댓글남기기