비관적 락 Pessimistic Lock
조회 시점에 배타 락(Exclusive lock)을 획득하여 조회, 수정, 삭제가 불가능하게 하고 commit/rollback 시점에 반납하는 방식으로 작동
기본적으로 트랜잭션 간의 충돌이 자주 발생할 수 있다고 가정하는 방식
세 가지의 잠금 모드가 사용됨 (Shared Lock, Exclusive Lock, Update Lock)
- 공유 락 Shared Lock: 읽기 작업을 위한 락. 여러 트랜잭션이 동시에 데이터를 읽을 수 있음
- 배타적 락 Exclusive Lock: 쓰기 작업을 위한 락. 한 트랜잭션만 데이터 수정 가능
사용자는 적용된 잠금에 따라 잠긴 레코드를 읽을 수 있음
- 장점
- 데이터 정합성을 보장할 수 있음
- 충돌이 빈번하게 일어난다면 롤백 횟수를 줄일 수 있기 때문에 낙관적 락 보다 성능이 좋을 수 있음
- 단점
- 데이터 자체에 별도의 락을 잡기 때문에 동시성이 떨어져 성능저하가 발생할 수 있음
- 두 트랜잭션이 서로의 자원이 필요한 경우 이미 락이 걸려있으므로 데드락이 일어날 수 있음
- 애플리케이션 확장성에 영향을 미침
- JPA
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
- MySQL에서는 FOR UPDATE 문을 통해 해당 레코드에 독점 잠금을 적용할 수 있음
비관적 동시성 처리 방식은 트랜잭션을 처리하기 전에 충돌을 차단하므로 나중에 트랜잭션을 롤백할 필요가 없으므로 데이터 충돌이 많은 애플리케이션에 적합함
낙관적 락 Optimistic Lock
실제 락을 사용하지 않고 데이터 조회 후 update 시 현재 버전이 최신인지 확인하며 업데이트 하는 방식으로 작동
트랜잭션 간의 충돌이 드물게 발생한다고 가정하고 데이터 접금 시점에 락을 걸지 않고 비동기화된 방식으로 진행할 수 있도록 허용
사용자의 변경 사항이 데이터베이스에 커밋되기 직전에 트랜잭션 간의 충돌이 있는지 확인
충돌이 있는 경우 사용자가 개입하여 애플리케이션 단에서 트랜잭션을 수동으로 완료해야 한다
타임스탬프 혹은 버전 번호를 사용하여 동시성 제어. 사용자가 데이터베이스에 변경 사항을 커밋하려고 할 때 두 항목을 비교하여 어떤 레코드를 유지할지 결정하는 방식
- 장점
- 별도의 락을 잡지 않으므로 비관적 락 보다 성능이 좋을 수 있음
- 교착상황이 없음
- 애플리케이션 확장을 위한 지원 제공
- 한 번에 여러 사용자에게 서비스 지원 제공
- 단점
- 동시성 처리 로직을 수동으로 구현해야 함
- 버전 또는 타임스탬프 유지 관리 필요
- 트랜잭션의 롤백에 대한 비용이 비쌈
- JPA: 엔티티에 @Version private Long version; 컬럼을 추가하여 적용 가능
- 어노테이션을 통해 간단하게 CAS(compare-and-set) 연산 구현이 가능하도록 지원
- Entity에서 낙관적 락의 충돌을 감지할 경우 OptimisticLockException 을 던지며 이 예외로 인해 활성 트랜잭션은 롤백을 위한 롤백마크가 표시
- 권장되는 예외처리 방법에서는 Entity를 다시 로드하거나 새로고침하여 업데이트를 재 시도하는 방법
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version; //CAS 연산을 위한 version 정보
@Lock(LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
}
- MySQL: 테이블에 버전 번호 혹은 타임스탬프를 기록하기 위한 컬럼을 추가하여 구현
분산 락
데이터 베이스 등 공통된 저장소를 이용하여 자원이 사용 중인지 체크
클러스터의 여러 노드에 분산되어 있는 데이터베이스, 파일 또는 네트워크 리소스와 같은 공유 리소스에 대한 액세스를 규제하는 데 사용
분산 잠금은 여러 노드가 동일한 리소스를 동시에 읽고 쓰는 것을 방지하기 위해 사용되는 방법
대규모 분산 시스템 혹은 고가용성 및 성능이 중요한 시스템에서 개별 노드들이 다른 노드가 무엇을 하고있는지 알 수 있도록 동기화하는 메커니즘을 통해 동시성 문제 해결
ZooKeeper Distributed Lock
- ZooKeeper는 구성 정보 유지, 이름 지정, 분산 동기화 제공, 그룹 서비스 제공을 위한 중앙 집중식 서비스
- 분산 애플리케이션 관리를 위한 코디네이션
- 임시 순차 노드를 기반으로 ZooKeeper에서 노드로 잠금을 구현
- 여러 클라이언트가 동시에 잠금을 획득하면 여러 개의 임시 시퀀스 노드가 순차적으로 생성되며 이 때 첫 번째 시퀀스 번호를 가진 노드만 성공적으로 잠금을 획득할 수 있다
- 주키퍼를 사용하여 구현된 락은 일종의 우선순위 큐와 유사한 동작을 하며 이는 각 클라이언트가 직접적으로 우선순위를 결정하는 것이 아닌 생성된 시퀀스 번호를 통해 우선순위가 결정된다
- 락을 획득하지 못한 다른 노드들은 다음으로 낮은 시퀀스 번호를 가진 락 디렉토리의 경로에 대해 exists()함수를 호출하고 watch 플래그를 설정함. 이후 잠금이 해제되면 잠금을 획득할 수 있음
Redis Distributed Lock
- Single Thread 이므로 CPU 소모의 최소화, Context Switching 비용의 최소화, 멀티스레드 애플리케이션 환경에서 스레드 동기화를 위해 필요한 잠금에 대한 오버헤드를 줄일 수 있음
- 인 메모리로 실행되기 때문에 지연시간이 짧고 처리량이 높음
- 분산 잠금을 효과적으로 사용하기 위해 필요한 세 가지 속성을 보장함
- Safety property: 상호 배제로 특정 순간에는 한 클라이언트만 잠금을 보유할 수 있음
- Liveness property A: 교착상태가 없음. 충돌이 일어나더라도 결국 항상 잠금을 획득
- Liveness property B: 내결함성. 대부분의 레디스 노드가 가동중이면 잠김 획등이 가능
- Lua Script를 허용하여 atomic한 작업 처리 지원
- 여러가지 명령어를 조합할 수 있을 뿐 아니라 스크립트 자체가 하나의 큰 명령어로 해석되기 때문에 스크립트가 atomic하게 처리됨
- 이를 통해 애플리케이션 로직의 일부를 레디스 내에서 실행할 수 있으며 atomic 한 작업으로 레디스 데이터에 대한 복잡한 작업을 수행할 수 있음
- SETNX 명령을 통해 분산 락을 구현할 수 있으며 해당 명령어는 SET if Not eXists로 특정 Key에 Value가 존재하지 않을 경우에는 값을 설정할 수 있음
- DeadLock 방지를 위해 EX명령어를 활용해 Lock 의 Timeout 설정까지 가능함
Spin Lock
- Lettuce는 분산락 기능을 제공하지 않기 때문에 필요한 경우 Spin Lock 방식등을 활용해 직접 구현해 사용하는 방식
- 조금만 기다리면 바로 쓸 수 있는데 굳이 Context Switching으로 부하를 줄 필요가 있는가 라는 컨셉으로 개발된 것으로 진입이 불가능할 때 Context Switching을 하지 않고 루프를 돌며 재시도 하는 것을 의미함
- Spin Lock은 Lock을 얻을 수 없다면 계속해서 Lock을 확인하며 바쁘게 기다리는 busy waiting이라는 특성이 있음
- 이는 무한적으로 루프를 돌며 최대한 다른 스레드에게 CPU를 양보하지 않는다는 것
- 즉 Lock이 곧 해제되어 사용가능해질 경우 Context Switching이 줄어 CPU의 부담을 줄일 수 있다는 장점
- Lock이 오랫동안 유지되면 오히려 CPU의 시간을 많이 소모할 가능성이 높다
- 무한적으로 루프를 돌기보다 일정시간 Lock을 획득할 수 없다면 잠시 sleep 하는 방식의 Exponential Backoff(지수 백오프)혹은 ConstantBackOff 알고리즘을 사용하는 것이 좋다
- 예상 대기 시간이 짧은 경우 유용하며 이를 위해 sleep에 대한 적절한 시간 설정이 중요하다
Publish/Subscribe
- Redission Client가 제공
- pub/sub은 게시와 구독이라는 의미를 가지고 있으며 이는 실제 서버리스 및 마이크로서비스 아키텍처에서 비동기적으로 통신하기 위한 소프트웨어 메시징 패턴
- 수신자가 발생자의 새 메시지를 반복적으로 폴링할 필요가 없다는 효율적인 측면
- Lockdmf pub/sub 채널을 사용하여 제공하며 잠금을 획득하기 위해 대기 중인 모든 Redission 인스턴스의 다른 스레드에 알림을 보내는 방식을 사용
- 락을 획득한 Redission 인스턴스가 충돌하며 해당 잠금은 획득된 상태에서 영원히 멈출 수 있기 때문에 이를 방지하기위해 잠금 감시를 유지하며 잠금 보유자인 인스턴스가 살아있는 동안 잠금 만료를 연장함. 기본적으로 잠금 감시 시간 제한은 30초이며 변경 가능
- pub/sub 방식은 락이 해제될 때마다 subscribe 중인 클라이언트들에게 락 획득을 시도해도 된다는 알림을 보내기 때문에 Spin Lock 과는 다르게 지속된 요청으로 인한 부하가 발생하지 않는 다는 장점이 있다
- 사용자가 별도의 Retry 로직을 작성하지 않아도 된다.
- 성능이 좋지만 현재 활용중인 redis가 없다면 별도의 구축 비용과 인프라 관리 비용이 발생
Redlock Algorithm
- Single Redis로 구축되어 있을 경우 레디스에 문제가 생긴다면 이는 SPOF 즉 단일 장애점이 될 수 있음
- 이러한 문제를 해결하기 위해 레디스는 Cluster 혹은 Sentinel 과 같은 방식을 통해 고가용성을 제공하고 있음
- 여러 노드의 레디스가 동작하는 경우에도 Master-Replica의 동기화 작업 중 Master에 장애가 발생한다면 락이 유실되는 것을 막을 수 없다
- 이를 위해 레디스는 Redlock Algorithm을 제공하며 이는 Cluster 환경의 여러 노드의 레디스 마스터 노드들이 락을 획득하거나 해제할 수 있도록 보장한다.
Redlock Algorithm
현재 시간을 밀리초 단위로 가져옴
모든 N개의 레디스 인스턴스를 순차적으로 동일한 키 이름과 랜덤 값을 사용하여 얻으려 시도함
이 때 인스턴스와 통신하는 동안 긴 block을 하지 않도록 전체 잠금 자동 해제 시간에 비해 작은 타임아웃을 사용
이를 통해 인스턴스가 사용 불가 상태일 경우 빠르게 다음 인스턴스와 통신을 시도
과반수의 인스턴스에서 잠금을 얻고 잠금을 얻기위해 소요된 총 시간이 잠금 유효시간보다 작은 경우 잠금을 획득
만일 클라이언트가 인스턴스의 절반 이상을 잠그지 못했거나 유효시간이 음수인 경우에 모든 인스턴스는 잠금을 해제하려고 시도
---
마틴 클레프만의 한계 분석
- Redlock 알고리즘은 시간적 가정에 의존하여 작동하며 네트워크 지연, 프로세스 일시 중단, 시스템 클럭 오차 등에 민감함 (GC로 인한 stop-the-world 등)
- 동기적 시스템 모델에 의존한다고 가정하는데 이는 실제 시스템 환경에서는 적용하기 어려움
- 안정성 및 신뢰성이 취약한 점을 지적하며 이를 해결하기 위해서는 적절한 합의 알고리즘을 사용해야 한다고 제안
- 이는 클라이언트가 잠금을 획득할 때 마다 증가하는 펜싱 토큰 또는 버전 등의 기능이 Redlock에는 없기에 클라이언트가 중단되거나 지연될 경우 race condition이 발생하여 동시성 문제가 생길 수 있어 이러한 알고리즘이 필요하다 하는 것
MySQL: GET_LOCK 과 RELEASE_LOCK 을 이용하여 분산 락 구현
- DataSource 를 주입받아 JDBC를 이용하여 직접 구현
- GET_LOCK, RELEASE_LOCK 모두 동일한 Connection 사용. Lock 획득부 ConnectionPool, 로직 수행부 ConnectionPool q분리
- 이미 MySQL을 사용하고 있다면 별도의 비용 없이 사용 가능
'DB' 카테고리의 다른 글
Partitioning (0) | 2025.01.18 |
---|---|
B+Tree, B-Tree (0) | 2025.01.17 |
Index Range Scan (0) | 2025.01.16 |
Index의 랜덤 I/O와 순차 I/O (0) | 2025.01.16 |
Multi-column Index (0) | 2025.01.16 |