[Spring Boot] 분산 서버 동시성 제어
Spring Boot공유된 자원에 대하여 여러 서버에서 한 번에 요청이 들어왔을 때 동기화 속도가 처리 속도를 따라가지 못하면서 데이터 정확성이 깨지고, 데이터 동시성 문제 발생한다.
Lock
동시성 제어를 위한 상호배제 기법 중 하나
데이터베이스 서버 구성에 따른 Lock
분산 서버 + 싱글 데이터베이스
- 낙관적 잠금
- 비관적 잠금
- 분산 잠금
분산 서버 + 분산 데이터베이스
- 분산 잠금
비선점 잠금(Optimistic Lock, 낙관적 잠금)
트랜잭션 출동이 발생하지 않을 거라고 보고 잠금을 거는 기법
JPA는 자체적인 버전 관리 기능(버저닝)으로 잠금 처리한다.
- Entity에 @Version 이나 @OptimisticLocking 어노테이션이 설정되어 있으면 암시적 잠금이 자동 적용된다.
- 삭제 쿼리가 발생할시에는 암시적으로 해당 로우에 대한 행 배타잠금(Row Exclusive Lock)을 제공한다.
트랜잭션 시작과 종료 시점에 버전을 확인한다.
- 종료 시점에 버전이 다르면 롤백을 하는데 처리된 만큼 롤백해야 하기 때문에 자원 소모가 크다.
방법 1. Annotation @OptimisticLocking 사용
kotlin
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@Entity
class Account()
방법 2. Annotation @Version 사용
kotlin
@Entity
class Account(
@Version
val version: Int
)
@Transactional
@Lock(value = LockModeType.OPTIMISTIC)
fun creatAccount()
- NONE: Entity 필드에 Version이 있으면 낙관점 잠금을 적용한다.
- OPTIMISTIC_FORCE_INCREMENT: 낙관적 잠금과 함께 버전 정보가 증가한다.
- READ: JPA 1 호환성 용도
- OPTIMISTIC: 낙관점 잠금
- WRITE: JPA 1 호환성 용도
- 버전 정보가 변경되어 트랜잭션이 충돌하면 OptimisticLockException이 발생한다.
선점 잠금(Pessimistic Lock, 비관적 잠금)
트랜잭션 충돌이 반드시 발생한다는 가정하에 미리 잠금을 거는 기법
DBMS Lock 기능을 사용한다.
교착 상태가 발생할 수 있어서 최대 시간 설정이 필요하다.
kotlin
@Transactional
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
fun creatAccount()
- PESSIMISTIC_READ: 공유 잠금. 다른 트랜잭션에서 데이터를 읽을 수 있지만, 변경하거나 삭제하지 못하게 한다.
- PESSIMISTIC_WRITE: 배타적 잠금. 다른 트랜잭션에서 데이터를 읽거나 변경, 삭제하지 못하게 한다.
- PESSIMISTIC_FORCE_INCREMENT: 배타적 잠금과 동일. Entity에 @Version이 지정되어 있으면 버전 정보를 증가시킨다(버저닝)
분산 잠금(Distributed Lock)
데이터베이스 등 공통된 저장소를 이용하여 자원이 사용 중인지 확인한다.
@Transactional은 Spring AOP 방식이기 때문에 분산락과 동시에 동작하지 않는다.
- Lock을 해제하기 전에 COMMIT을 해줘야 동시성 문제가 발생하지 않는다.
Redis (Remote Dictionary Server)
Dictionary(key-value)구조로 된 비정형 데이터를 메모리에 저장하고 관리하는 In-Memory 기반 비관계형 데이터베이스 관리 시스템.
NoSQL로 분류되기도 한다.
비동기식 복제 방식으로 이중화를 지원한다.
데이터베이스, 캐시, 메시지 브로커 및 스트리밍 엔진으로 사용된다.
- Lettuce
- Netty(비동기 이벤트 기반 자바 네트워크 프레임워크)를 기반으로 만들어진 Redis 클라이언트
- 사용자가 직접 Redis 명령어 setnx를 사용해서 스핀 락 형태로 구현하여 사용한다.
- Lock을 획득할 때까지 지속적으로 Lock 획득을 시도한다.
- Lock이 정상적으로 해제되지 않으면 무한 루프에 빠질 수 있다. (DeadLock 발생)
- Lock 획득을 시도할 때마다 Redis에 Lock이 존재하는 지 요청을 보내기 때문에 Redis에 부하를 주어 응답시간이 지연될 수 있다.
- 무한 루프에 빠지지 않도록 최대 시도 횟수나 만료 시간을 구현해야 한다.
- Redission
- Netty(비동기 이벤트 기반 자바 네트워크 프레임워크)를 기반으로 만들어진 Redis 클라이언트
- pub/sub(발행/구독) 기능을 사용한다.
- Lock 만료시간이 구현되어 있어서 무한 루프에 빠지지 않는다.
- Lua Script를 사용하여 자체 TTL을 적용하고 있다.
- Jedis
- 성능과 사용 편의성을 위해 설계된 Redis용 Java 클라이언트
- Spring Boot에서 Redis 기본 클라이언트로 사용되었으나, 스레드에 안전하지 않고 피드백이 거의 이루어지지 않아 Spring Boot 2.0 부터 기본 클라이언트가 Lettuce로 바뀌었다.
Apache Zookeeper
Coordination Service System
부분 실패를 안전하게 처리하기 위한 분산 처리 도구 제공
데이터를 znode에 계층적 트리 형태로 관리
Hbase, Kafka, Hadoop, Kubernetes 등 많은 오픈소스에서 활용
Reference
MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리
redis 설치 및 redisson을 이용한 분산락 구현
레디스와 분산 락(1/2) - 레디스를 활용한 분산 락과 안전하고 빠른 락의 구현
[주키퍼, Zookeeper] 아파치 주키퍼(Apache Zookeeper) 소개 및 아키텍처