Advisory Lock 으로 유연하게 동시성 제어하기

김법우·2024년 8월 7일
0

Database

목록 보기
11/12

Postgres 만의 잠금, Advisory Lock

When?

두 요청이 직렬화 되야하고 애플리케이션 레벨에서 동시성을 제어해야하는 경우

서버 개발을 하면서 많은 동시성 문제들을 만났던 기억을 되짚어보자. 내가 개발했던 대부분의 상황에서는 API 동시 요청으로 인해 충돌이 일어나더라도 크게 문제가 되지 않는 경우가 많았다.

하지만 일부 동시성 문제 발생시 심각한 장애나 서비스 사용에 문제가 생기는 경우 충돌이 발생하지 않도록 하거나 충돌 발생을 감지하고 해결해야한다. 오늘 다룰 Advisory Lock 은 충돌을 회피하는, 일종의 비관적 잠금 전략아주 유연하게 애뮬레이션 할 수 있게 하는 좋은 도구다.


What?

Postgres 에서 제공하는 애플리케이션 레벨 제어 잠금으로 트랜잭션 혹은 세션 단위로 획득하며 배타적 잠금을 애뮬레이션한다

Postgres 에서는 다른 DBMS 와 마찬가지로 다양한 종류의 명시적 잠금을 제공한다.

DML, DDL 등에 의해 개발자가 명시적으로 잠금을 획득하지 않더라도 DBMS 가 데이터 일관성을 보장하기 위해 자동으로 획득하는 잠금이 있는 반면, Advisory Lock 은 애플리케이션에게 요구되는 동시성 문제를 해결하기 위해 제공되는 명시적 잠금의 일종이다.

Point

  • 애플리케이션 레벨 제어
  • 트랜잭션 혹은 세션 단위 획득
  • 배타적 잠금

유즈 케이스

✔️ 외부 API 호출 작업을 포함하는 요청간 직렬화

💡 상황 가정

요청을 받으면 외부 API에 의존해 로직을 처리하는 프로세스가 있다고 가정하자.

해당 외부 API 는 동시에 요청할 경우 문제가 발생하므로 동시에 요청할 수 없도록 외부 API 가 요청 중인 경우 동일한 요청은 대기하거나 실패시키는걸 목표로 한다. (외부 결제 모듈을 사용해 결제 요청을 보내는 상황을 떠올려보자)

나는 외부 API 에만 사용했지만 파일 시스템 접근 등 기존에는 DB 에 접근하지 않는 작업들도 트랜잭션을 여는데 부담이 없는 처리 시간이라면, AD Lock 을 활용해 효과적으로 동시성 문제를 해결 할 수 있다고 생각한다.


🔥 Advisory Lock 으로 해결하기

Advisory Lock 은 배타락을 애뮬레이션하므로 해당 프로세스는 외부 API 를 호출하기전 Advisory Lock 을 획득하도록 한다. 외부 API 의 응답을 받고 사후처리가 모두 완료되면 잠금을 보유한 프로세스는 잠금을 반환하면 된다.


⛔ 주의

  • AD Lock 을 획득하는 시점은 일관성을 보장하고자 하는 외부 API 호출 전이어야한다.
  • AD Lock 은 세션 혹은 트랜잭션 단위로 획득 가능하므로 적절(최소)한의 범위로 획득한다.
  • AD Lock 을 획득하는 키값을 해당 요청으로부터 얻을 수 있어야한다. 뒤에서 다시 다루겠지만 Postgres 의 AD Lock 의 식별자는 int4, int8 의 정수값이다. 가령 10 이라는 정수값으로 잠금을 얻었다면, 다른 프로세스가 10 이라는 정수값으로 잠금 획득 시도시 충돌한다는 뜻이다.

✔️ 논리적으로 식별되는 요청간의 복잡한 동시성 제어

💡 상황 가정

단순히 요청한 특정 유저 단위가 아닌 유저가 속한 그룹 단위로 동시성을 보장해야하는 프로세스가 있다고 가정하자. 유저 A, B, C 는 그룹 1 일때 동일한 그룹이라면 충돌시켜야하는 상황이라고 할 수 있겠다.

특정 그룹의 유저가 글을 작성 중이라면, 동일 그룹내 유저들은 글을 작성 할 수 없어야한다고 가정하자.


🔥 Advisory Lock 으로 해결하기

이런 상황에서 해당 요청을 직렬화하기 위해 추가적인 데이터 구조를 정의하거나, 상태 관리를 더하는 것보다 AD Lock 을 사용하면 더욱 간단하고 유연하게 처리 할 수 있다.

POST v1/post
Header : user A Token -> Key 10 으로 잠금 획득 성공 -> 글 생성

POST v1/post
Header : user B Token -> Key 10 으로 잠금 획득 요청 -> 충돌

⛔ 주의

  • 앞서 다루었듯이 AD Lock 을 획득하는 키값을 해당 요청으로부터 식별하고, 얻을 수 있어야한다.

Advisory Lock 의 실제 사용법

기본적인 사용법

트랜잭션 레벨의 제어

-- 충돌시 대기
begin;
select pg_advisory_xact_lock(10);
commit;

-- bool 값 반환
begin;
select pg_try_advisory_xact_lock(10);
commit;

세션 레벨의 제어

select pg_advisory_lock(10);

-- 세션 레벨에서는 자동으로 AD Lock 이 반환되지 않음
select pg_advisory_unlock(10);

해시 함수를 사용한 AD Lock Key 생성

결국 AD Lock 을 활용하기 위해서는 직렬화할 요청을 식별할 키값을 획득해 AD Lock 를 걸어야한다.

Postgres AD Lock 획득 함수는 int4, int8 의 정수값만을 받기 때문에 식별 키값이 문자열이라면 곤란한 상황이 생긴다. 문자열을 서버 자체적으로 정수값으로 해시해 사용 할 수 있겠지만 해시 함수를 활용하면 간단하게 구현 할 수 있다.

md5 함수 사용

SELECT pg_advisory_xact_lock(
  ('x' || md5('user_10'))::bit(64)::bigint
);
  • md5(식별자) 를 통해 128비트 길이의 해시값을 획득
  • 64비트를 뜯고 x 를 붙여 64비트 16진수를 획득
  • 16진수를 64비트 정수값으로 타입 캐스팅

hashtext 함수 사용

SELECT pg_advisory_xact_lock(
  hashtext('user_10')
);
  • 32비트 크기의 정수 해시값 생성

마치며

AD Lock 은 적재적소에 사용하면 동시성 문제를 해결하는데 있어서 서비스 복잡도를 크게 낮출 수 있는 좋은 도구라는 생각을 한다. 행 잠금이나 테이블 잠금과는 달리 잠금을 걸 행이나 테이블이 존재하지 않아도 애플리케이션 레벨에서 논리적으로 직렬화해야할 요청을 쉽게 직렬화하기 때문이다.

하지만 이런 특성 때문에 남용시에는 많은 사이드 이펙트가 발생할 수 있으므로 주의 해야겠다는 생각도 든다. 예를 들어 실제로 행 수준에서 배타락을 걸어 직렬화를 해야하는 상황인데 AD Lock 을 쓰기는 힘들다.

다른 모든 조회 로직 혹은 앞으로 추가될 로직에서 명시적으로 AD Lock 을 획득할 수는 없기 때문이다.

profile
개발을 사랑하는 개발자. 끝없이 꼬리를 물며 답하고 찾는 과정에서 공부하는 개발자 입니다. 잘못된 내용 혹은 더해주시고 싶은 이야기가 있다면 부디 가르침을 주세요!

0개의 댓글