Post

[DB] DB Lock

서론

트랜잭션 격리 수준에서 각 트랜잭션끼리 서로 어떤 기준으로 격리되어야하는 지 알아보았다. 트랜잭션 격리 수준이란 여러 트랜잭션이 동시에 테이블, 레코드를 접근하지 못하도록 막는 규칙을 의미한다.

그렇다면 Lock은 무엇일까?

트랜잭션 격리 수준이라는 규칙을 구현할 수 있으며, 여러 커넥션에서 동시에 변경할 수 없도록 데이터를 잠금하는 기법을 Lock, Locking이라고 칭한다.

이 Lock이라는 기법은 DB 레벨에서의 Lock이 있고, 어플리케이션 레벨에서의 Lock으로 나뉠 수 있다.

이 포스팅에서는 DB Lock에 대한 기본적인 개념과 데드락, 블로킹에 대해 다뤄본다.

그렇다면 트랜잭션과 Lock의 차이는?

Lock과 트랜잭션은 비슷한 개념 같지만 잠금은 동시성을 제어하기 위한 기능이고, 트랜잭션은 데이터의 정합성을 보장하기 위한 기능으로 목적이 살짝 다르다.

DB 레벨에서의 Lock

공유 락 (Shared Lock)

Read Lock, S Lock으로 불리며, 데이터를 변경하지 않는 읽기 작업을 위해 접근을 막는(잠그는) 기법을 의미한다.

  • 한 커넥션(세션)에서 데이터를 조회할 때에 공유 락을 걸었다면, 다른 커넥션에서 읽기 전용으로 공유 락을 걸어 접근할 수 있다.(허용된다.)
  • 한 커넥션(세션)에서 데이터를 조회할 때에 공유 락을 걸었다면, 다른 커넥션에서 데이터를 수정하기 위해 배타적 락을 걸 수는 없다.
    • 조회에 대한 Lock이 걸려있기에 데이터의 변경이 발생하면 안된다.

“내가 이미 데이터를 읽고 있는 동안 너도 데이터를 읽을 수는 있는데, 변경은 할 수 없다.(배타적 락 걸지 마라.)”

배타적 락 (Exclusive Lock)

Write Lock, X Lock으로 불리며, 데이터를 변경하기 위해 접근을 막는 기법을 말한다.

  • 한 커넥션(세션)에서 데이터를 조회할 때에 배타적 락을 걸었다면, 다른 커넥션에서 해당 데이터를 조회하면 결과가 달라져 데이터 정합성이 깨질 수 있기 때문에 공유 락 획득을 막는다.
  • 한 커넥션(세션)에서 데이터를 조회할 때에 배타적 락을 걸었다면, 다른 커넥션에서 해당 데이터를 수정할 때 결과가 달라져 데이터 정합성이 깨질 수 있기 때문에 배타적 락 획득을 막는다.

“내가 데이터를 변경하고 있는 동안 해당 데이터를 읽거나 변경하려고 선점할 수 없다.(공유 락 배타적 락 걸지 마라.)”

블로킹

보통 DB 작업을 수행할 때 데이터의 무결성과 정합성을 보장하기 위해 트랜잭션이 사용된다. Lock도 하나의 트랜잭션 안에서 걸리고 해제되게 된다.

블로킹은 Lock 간의 경합이 발생해서 특정 트랜잭션이 작업을 진행하지 못하고 대기하는 상태를 의미한다. blocking

블로킹이 발생하는 조건

  • 특정 데이터에 공유 Lock이 설정된 상태에서 해당 데이터에 배타적 Lock을 걸려고 할 때
  • 특정 데이터에 배타적 Lock이 설정된 상태에서 해당 데이터에 공유 Lock을 걸려고 할 때
  • 특정 데이터에 배타적 Lock이 설정된 상태에서 해당 데이터에 배타 Lock을 걸려고 할 때

MySQL에서 블로킹 예시

sharedlock

먼저 특정 트랜잭션을 시작한 후(begin)에 공유 Lock인 for share 키워드를 통해 member 테이블의 데이터를 조회한다.

exclusivelock

그 이후 다른 트랜잭션에서 배타 Lock인 for update 키워드를 이용해서 member 테이블을 조회하려 하면 Lock wait timeout이 발생한다. 블로킹이 일어난 것이다.

이를 해결하려면 다음에 실행된 트랜잭션이 Lock wait timeout에 걸리기 전에 이전 트랜잭션을 rollback시키거나 commit되어야 해결이 된다.

실무에서 블로킹이 발생된다면 해당 데이터에 대한 서비스의 로직이 모두 지연될 수 있고 이는 서비스에 악영향을 미칠 것이다…

블로킹 해결 방안

  • 트랜잭션의 단위를 작게 해야한다.
  • 동일한 데이터를 동시에 변경하는 로직을 설계하지 않는다.
    • 낙관적 락, 비관적 락으로 해결한다.
  • 서비스의 트래픽이 높아지는 시기에는 대용량 데이터 작업을 수행하지 않거나 작은 단위로 쪼개서 실행한다.

데드락 (DeadLock)

데드락은 두 트랜잭션이 모두 블로킹 상태가 되어 서로의 블로킹을 해결할 수 없는 상태이다.

트랜잭션 B가 트랜잭션 A에 의해 블로킹이 되었다고 가정하면, A 트랜잭션이 커밋이나 롤백이 되어야 B의 블로킹이 해결이 될 것이다.
하지만 이 상태에서 트랜잭션 A도 트랜잭션 B에 의해 블로킹이 된다면 마찬가지로 B 트랜잭션이 커밋, 롤백이 되어야 블로킹이 해결될 것이다.
두 트랜잭션이 모두 서로의 트랜잭션 종료를 대기하는 상태가 되어 데드락(=교착 상태)이 발생된다.

데드락의 예시

transaction 1 - update a row
transaction 2 - update a,b,c,d,e row (waiting for transaction 1 to free a)
transaction 1 - update b row (waiting for transaction 2 to free b)
DeadLock! - example 참고

위 예시를 보면, 복수의 데이터 row를 update하는 로직을 위해 배타적 Lock을 순서대로 걸어야하는데,(transaction 1)
다른 트랜잭션에서 이미 대상 row에 대한 배타적 Lock을 걸어놨다.(transaction 2)

대상 row에 이전 트랜잭션이 접근해서 배타적 Lock을 걸려고 하면, 데드락이 발생한다.

데드락 해결 방안

  • Dirty Read
    • 트랜잭션 격리 수준을 완화하여 commit되지 않은 데이터에 접근할 수 있도록 허용한다.
  • 데드락 예방
    • Lock wait timeout 제한 시간을 줄인다.
    • 각 트랜잭션이 실행되기 전에 대상 데이터를 모두 lock을 걸고, 걸지 못한다면 lock을 반납하도록 하여 데드락을 예방한다.
      • 병행성을 희생하며, 기아 현상(starvation)이 발생할 수 있다.

starvation - 어떤 transaction이 무한정 수행되지 않는 현상. 선착 처리(First-come First-served) 큐를 사용하여 해결한다.
First-come First-served - lock 요청 순서에 따라 lock을 걸 수 있도록 한다.

  • 데드락 회피
    • 자원을 할당할 때 timestamp를 이용해서 데드락을 회피한다.
    • Wait-Die wait-die
      • 비선점기법이다.
      • timestamp가 더 빠른 트랜잭션이 timestamp가 느린 트랜잭션이 점유한 자원에 접근한다면 대기한다.(wait) 하지만 timestamp가 더 빠른 트랜잭션이 점유한 자원에 접근한다면 롤백 후 재시도한다.(die) 롤백은 여러 번 발생할 수 있다.
    • Wound-Wait wound-wait
      • 선점기법이다.
      • timestamp가 더 빠른 트랜잭션에 대한 접근은 기다리지 않는다. 접근에 대한 lock을 뺏어서 선점하고 롤백시킨다.(wound) 하지만 timestamp가 비교적 더 느린 트랜잭션이 점유하고 있는 자원에 접근한다면 대기한다.(wait)
  • 데드락 빈도수를 줄이는 방법
    • 트랜잭션을 자주 커밋한다.
    • 정해진 순서대로 테이블에 접근한다.
    • 데드락 예방 목적이 아닌 잠금 획득 (select ~ for update)을 피한다.
    • 테이블 단위의 잠금을 획득하여 동시성을 낮추고 교착 상태를 피한다.

결론

DB에서의 기본적인 Lock의 형태와 블로킹, 데드락에 대해 다뤄보았다.
이후에는 낙관적 락, 비관적 락과 JPA, MySQL, MySQL의 기본 엔진인 innoDB에서의 Lock 구현 기법을 다뤄보고자 한다.

참고

This post is licensed under CC BY 4.0 by the author.