Search
🔒

DB Lock

락(Lock)이란?

여러 커넥션에서 동시에 동일한 자원을 요청할 경우 하나의 커넥션 변경하게 해주고 나머지 커넥션은 막는 역할
동시성을 제어하기 위한 기능이다.

자원에 대한 접근 권한에 따른 분류

공유 락 (Shared Lock, Read Lock, S-Lock)

공유 락은 데이터를 변경하지 않는 읽기 작업을 위해 잠그는 락을 말한다.
공유락은 읽기작업을 위해 거는 것이기 때문에 다른 세션의 공유락 획득을 허용한다. (여러 트랜잭션의 접근을 허용한다)
하나의 세션에서 읽기작업을 수행할 때, 다른 세션에서 쓰기 작업을 한다면 데이터의 정합성이 깨지므로 다른 세션의 베타 Lock 획득을 막막는다.
→ 공유락을 걸면 다른 세션의 공유락 획득은 허용하고 베타락은 허용하지 않는다. (다른 트랜잭션의 S-Lock은 허용, 다른 트랜잭션의 X-Lock은 허용 x)
즉, 내가 데이터를 읽는 동안 이거 읽을 수는 있어. 근데! 데이터 변경하기 위해 접근은 하지마! (X-Lock 걸지마!)

베타 락 (Exclusive Lock, Write Lock, X-Lock)

베타 락은 데이터를 변경하는 쓰기 작업을 위해 잠그는 락을 말한다.
베타 락은 데이터를 변경하는 작업이기 때문에 다른 세션에서 읽기 작업을 진행한다면 데이터의 정합성이 깨지므로 다른 세션의 공유 락 획득을 막는다.
→ 베타 락은 다른 세션의 공유 락, 베타 락 획득을 모두 막는다.
즉, 내가 데이터를 쓰는 동안 이 데이터에 대해 접근 자체를 하지마! (S-Lock, X-Lock 다 걸지마!)

데이터 충돌 처리 전략에 따른 분류

비관적 락(Pessimistic lock)

비관적 락은 Repeatable Read, Serializable 정도의 격리성 수준을 보장한다.
비관적 락이란 트랜잭션이 시작될 때 S-Lock 또는 X-Lock을 걸고 시작하는 방법이다.
즉 S-Lock만 걸어도 한 트랜잭션이 진행중일 때 다른 트랜잭션에서 수정이 불가능하다. (X-Lock 을 얻지 못하기 때문에)
수정을 하기 위해서는 모든 트랜잭션이 종료(commit)이 되어야 한다.
비관적 락은 여러 사용자가 동시에 동일한 데이터를 읽고 수정해야 하는 상황이 많을 경우 사용하는 것이 적합하다.
데이터의 일관성을 강제해야 하고 충돌을 방지해야 할 때 비관적 락을 사용한다.

낙관적 락(optimistic lock)

낙관적 락은 DB level에서 거는 Lock이 아니다!
낙관적 락은 전통적인 락 방식과는 다르게 Application Level에서 거는 Lock이다.
충돌이 거의 발생하지 않을 것이라 생각하고(낙관적) 충돌이 발생한 경우에 대비하는 방식
JPA기준으로 @Version 어노테이션을 사용하여 Application Level에서 Lock을 제어하는 방식이다.
낙관적 락은 데이터베이스에 락을 즉시 설정하지 않고, 데이터의 커밋 전에 충돌을 감지하는 방식이다.
충돌이 발생했을 때 특정 예외가 발생하며, 예외에 대한 후처리가 가능하다.
낙관적 락은 정리하자면, 충돌이 거의 발생하지 않은 경우에 혹시 충돌이 발생하더라도 후처리가 간단한 상황에 맞는 방식이다.
데이터를 읽을 때는 락을 설정하지 않으므로 읽기 작업이 많은 상황에서 성능을 향상시킬 수 있다.
DB락은 결국 Connection Pool을 잡고있는 것이기 때문에 분산락을 고려해 볼 상황도 생기는 것이다.

분산락

분산락이란?

분산락은 분산 시스템 환경에서 여러 서버가 동일한 자원에 대해 동시에 접근하는 것을 방지하기 위한 동시성 제어 메커니즘이다.
단일 서버 환경에서는 Mutex나 Semaphore로 자원 접근을 제어할 수 있지만 분산 환경에서는 각 서버가 물리적으로 떨어져 있기 때문에 이를 제어하는 것이 더 어렵다 여러 서버나 노드가 동시에 자원에 접근하면 데이터의 일관성이 깨질 수 있는데, 이를 방지하고 동시성 문제를 해결하기 위해 분산 락을 사용합니다.
분산 락은 각 노드가 락을 획득할 수 있을 때만 자원에 접근하도록 보장합니다.

구현하는 방식

1.
Lettuce 스핀락 방식으로 구현한다. Lock을 점유하는 시간이 짧으면 해당 방법이 유리하지만 스레드가 Lock을 오래 물고 있을 경우 CPU에 부담을 줄 수 있다.
redis의 setnx 명령어는 특정 key가 존재하지 않는 경우에만 값을 설정하는 명령어인데 이를 락처럼 사용할 수 있다.
setnx 명령어로 redis에 key를 저장할때까지(락을 획득할때까지) while문을 돌면서 락 획득을 시도한다.
락을 획득하면 로직을 모두 수행한 후에 redis에서 해당 키를 삭제한다.
이 방식의 문제점은 Lock 타임아웃 설정이 되지 않는다는 문제가 있다. setnx 방식은 TTL 설정을 할 수 없다.
따라서 Redis에 많은 부하가 발생할 수 있다. Redis 자체내에서 key값에 대한 TTL설정은 할 수 있다.
2.
Redisson 스핀락인 Lettuce와 다르게 스핀락 방식이 아닌 Pub/Sub 방식을 사용한다. Redis에 가해지는 부하를 줄일 수 있다. **Pub/Sub 방식** 구독자에게 락이 해제될 때마다 메세지를 전달하는 방식이다. 구독자들에게 메세지를 바로 보내기 때문에 따로 메세지를 보관하고 있진 않는다.
대기없는 tryLock을 통해 락 획득에 성공하면 true를 반환한다. 경합이 없다면 아무런 오버헤드 없이 락을 획득할 수 있다.
pub/sub 방식으로 메세지가 올 때까지 대기하고 있다가 락이 해제되었다는 메세지가 오면 대기를 풀고 다시 락 획득을 시도한다.
락 획득에 실패한다면 다시 락 해제 메세지를 기다린다.
이 프로세스는 TTL까지 반복한다.

분산락의 문제점과 해결방안

1.
데드락
문제 : 분산 락을 사용하는 동안 노드가 장애로 인해 락을 처리하지 못하면 데드락이 발생할 수 있다. 이로 인해 다른 노드들이 해당 자원에 접근하지 못하고 대기하는 상황이 발생할 수 있다.
해결방안 : Redis에서 TTL을 설정하여 락이 일정 시간 후 해제되도록 설정하여 데드락을 방지 할 수 있다.
2.
TTL과 작업시간의 불일치
문제 : 락의 TTL은 만료되었지만 작업이 아직 완료되지 않은 상태
해결방안 : TTL 시간을 작업이 완료될 수 있도록 충분히 찾아주거나 Watchdog Timer 등을 이용하여 락의 TTL 시간을 연장하는 방식을 사용할 수 있다.

Redis 분산락에서 순서를 보장하는 방법

1.
메세지에 순서 ID 포함
Publisher에서 걱 메세지에 고유한 순서를 보장하여 부여하는 것이다. 그러면 구독자는 메세지를 수신할 때 해당 순서 ID를 기반으로 메세지 순서를 조정할 수 있다.
2.
Redis의 대기열 FIFO Queue 방식으로 구현
Redis의 List 자료구조를 이용해 큐를 구현 대기열을 관리할 수 있다.