해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요

목차

1. Lock의 필요성
2. Lock의 종류
3. Lock의 동작 원리
4. Lock의 문제점과 해결책
5. 결론


개발자들은 종종 특정 기능을 개발할 때 동시성 이슈를 고려하지 않는 경우가 있다.

대부분의 상황에서는 동시성 이슈를 고민할 필요가 없지만, 포인트 차감이나 재고 관리 기능 등 특정 행을 동시에 수정하는 경우 동시성 이슈가 발생할 수 있다.

이때 별도의 락을 사용하지 않으면 동시성 이슈로 인해 의도하지 않은 수정이 발생할 수 있다.
따라서 데이터의 일관성과 무결성을 보장하기 위해 DB 락을 거는 작업은 매우 중요하다.
물론 모든 작업에 대해서 락을 거는 것은 효율성 측면에서 굉장히 비효율적이다.

그래서 무결성, 일관성이 보장되어야 하는 작업인 경우에 적절하게 락을 활용하는 것이 좋다.

1. Lock의 필요성

  • 데이터베이스 락은 다수의 트랜잭션이 동시에 데이터베이스의 동일한 자원을 수정하려고 할 때 발생할 수 있는 문제를 방지하기 위해 사용된다.
  • 락을 통해 데이터 무결성을 유지하고 데이터 일관성을 보장할 수 있다.

2. Lock의 종류

Shared Lock (공유 락)

  • 공유 락은 여러 트랜잭션이 동시에 데이터를 읽을 수 있도록 허용하지만, 데이터의 쓰기는 허용하지 않는 락이다.
  • 공유 락의 주요 목적은 데이터 일관성을 유지하면서도 여러 트랜잭션이 동일한 데이터에 접근할 수 있도록 하는 것이다.
  • 즉, 공유 락끼리는 같은 자원을 점유할 수 있지만 베타 락 (데이터 쓰기)의 점유는 허용하지 않는다.
  • select 구문 뒤에 LOCK IN SHARE MODEFOR SHARE 을 붙여서 공유락을 걸 수 있다.
  • 두 구문은 데이터베이스 시스템에 따라서 다르다.
  • SELECT ... LOCK IN SHARE MODE는 MySQL에서 사용
  • SELECT ... FOR SHARE는 SQL 표준에 따라 PostgreSQL, Oracle에서 사용
  • SELECT ... FOR SHARE는 SQL 표준이라서 조금 더 널리 사용되는 것 같다.

예시

테이블 생성 및 데이터

create table user_point
(
    id      bigint auto_increment primary key,
    points  bigint not null,
    user_id bigint not null
);
# 위와 같이 회원 포인트를 저장하는 테이블이 있다고 가정한다
INSERT INTO user_point (id, points, user_id) VALUES (1, 1000, 1);

트랜잭션1 - user_id 1에 공유 락 걸기

# 트랜잭션 시작 후 공유 락을 건다
START TRANSACTION;
select * from user_point where user_id = 1 FOR SHARE;

# 커밋 하지 않고 대기한다

트랜잭션2 - user_id 1에 공유 락 걸기

# 트랜잭션 시작 후 공유 락을 건다
# 트랜잭션 1에서 공유락을 걸었지만 같은 공유락끼리는 자원을 공유할 수 있다.
START TRANSACTION;
select * from user_point where user_id = 1 FOR SHARE;
commit;

트랜잭션3 - 포인트 차감

# update문을 실행하면 기본적으로 행수준에서 베타락을 건다.
START TRANSACTION;
update user_point
set points = points -1
where user_id = 1;
commit;
  • user_id 1 자원에 공유락이 걸려있기 때문에 트랜잭션 1,2가 commit 되면 트랜잭션 3의 작업인 포인트 차감을 수행할 수 있다.

Exclusive Lock (베타 락)

  • 특정 자원을 수정할 때 일관성, 무결성을 보장하기 위해 사용하는 락이다.
  • 트랜잭션 내에서 특정 자원에 베타 락이 걸리면 커밋되기 전까지 조회를 제외하고 해당 자원에 어떠한 락도 걸 수 없다.

트랜잭션1 - user_id 1에 베타 락 걸기

# 트랜잭션 시작 후 베타 락을 건다
START TRANSACTION;
select * from user_point where user_id = 1 FOR UPDATE;

# 포인트 차감
update user_point
set points = points -1
where user_id = 1;

# 커밋 하지 않고 대기한다
  • user_id 1에 베타락이 걸려 있기 때문에 다른 어떠한 락도 해당 자원을 점유할 수 없다.

트랜잭션2 - user_id 1에 공유 락 걸기

START TRANSACTION;
select * from user_point where user_id = 1 FOR SHARE;
commit;
  • 트랜잭션1에서 user_id 1에 베타락을 걸었기 때문에 대기한다.

트랜잭션3 - user_id 1에 베타 락 걸기

START TRANSACTION;
select * from user_point where user_id = 1 FOR UPDATE;
commit;
  • 트랜잭션1에서 user_id 1에 베타락을 걸었기 때문에 대기한다

트랜잭션4 - user_id 1 조회

START TRANSACTION;
select * from user_point where user_id = 1;
commit;
  • 락을 거는 것이 아닌 단순 조회 쿼리이기 때문에 조회가 가능하다.
  • 조회되는 값은 트랜잭션 격리 수준에 따라서 달라진다.

트랜잭션1 - commit

  • 트랜잭션 1이 커밋되는 순간 락 대기 큐에 저장된 값에 따라서 user_id 1 자원을 선점하게 된다.

Lock의 동작 원리

Lock 획득

  • 트랜잭션이 시작되면 수행하려는 작업(읽기, 쓰기)에 따라 공유 락 또는 베타 락을 걸 수 있다.
  • 예를 들어, 회원의 포인트를 차감하려고 할 때 베타 락을 걸어 먼저 해당 자원을 점유할 수 있다. 
  • 만약 이미 해당 자원이 선점되어 있다면, 현재 트랜잭션은 락을 획득할 수 있을 때까지 대기 큐에 추가되어 자원이 해제될 때까지 기다린다.
  • 하지만 락 대기 시간이 일정 시간을 초과하면 타임아웃이 발생하고 트랜잭션이 롤백된다.

Lock 해제

  • 락 해제는 일반적으로 트랜잭션이 커밋(commit)되거나 롤백(rollback)될 때 발생한다.

Lock의 문제점과 해결책

데드락 (Dead Lock)

  • 두 개 이상의 트랜잭션이 서로가 소유한 락을 기다리며 무한 대기 상태에 빠지는 현상이다.
  • 이는 개발 시 하나의 트랜잭션 크기와 지속시간이 길거나 여러 테이블에 락을 거는 경우 의도지 않게 발생하는 문제이다.  
  • 이를 해결하려면 락을 거는 경우 트랜잭션에 지속시간을 줄이고, 락의 범위를 줄여서 다른 트랜잭션에 영향을 끼치지 않게 해야한다.
  • 트랜잭션의 지속시간을 줄이려면 락을 걸고 수정하는 부분만 짧게 실행하고 트랜잭션을 종료하여 락을 반환하면 된다.

분산 DB 환경일 경우 문제점

  • 고가용성을 위해서 마스터 DB(write DB)가 여럿 존재할 경우 Lock을 거는 것이 힘들어 질 수 있다. 
  • 그 이유는 마스터 DB 끼리 Lock을 공유하려면 그에 대한 복잡성과 비용이 크기 때문인데 이를 해결하기 위해 나온 것이 분산락이다. 
  • 분산락은 Redis, Mysql, Zookeeper 등을 활용하여 구현할 수 있다.

결론

  • DB Lock은 데이터베이스의 무결성과 일관성을 유지하는 데 필수적인 메커니즘이다.
  • 락을 사용하면 동시성이 증가하기 때문에 처리속도 측면에서 안좋은 영향을 끼칠 수 있다.
  • 그래서 적절하게 전략을 세워서 무결성, 일관성이 꼭 보장되어야 하는 부분에 락을 사용하는 것이 중요하다.

+ Recent posts