일반적으로 분산 서버 환경에서 하나의 데이터베이스를 사용하더라도 여러 대의 서버가 해당 데이터베이스에 동시에 접근하고 데이터를 수정하려고 할 때는 여전히 동시성 문제가 발생할 수 있다.
이러한 상황에서 낙관적 락(Optimistic Locking)과 비관적 락(Pessimistic Locking)이 물론 유용할 수 있지만,
분산 락(Distributed Locking)이나 다른 분산 동시성 제어 메커니즘을 고려해야 하는 경우가 생길 수 있다.
그 이유는 다음과 같다.
- 데이터베이스 트랜잭션 범위: 여러 서버에서 하나의 데이터베이스에 접근할 때, 트랜잭션의 범위가 여러 서버에 걸칠 수 있다. 이 경우, 낙관적 락과 비관적 락만으로는 트랜잭션 일관성을 보장하기 어려울 수 있다.
- 동시성 제어의 복잡성: 분산 환경에서 여러 서버 간에 동시성을 관리하려면 각 서버에서의 락을 획득하고 해제하는 방법을 동기화해야 한다. 이러한 동기화는 분산 락 또는 다른 분산 동시성 제어 메커니즘이 필요한 이유 중 하나가 될 수 있다.
- 데이터 일관성 유지: 여러 서버 간의 데이터 일관성을 유지하려면 데이터의 동기화와 동시성 제어가 필요한데, 이러한 사항을 만족시키기 위해서는 분산 락 또는 데이터 일관성을 보장하는 다른 메커니즘이 필요할 수 있다.
- 복제 및 샤딩: 분산 데이터베이스 시스템에서 데이터의 복제와 샤딩을 사용하는 경우, 데이터 접근 패턴에 따라 다양한 복제 및 샤딩 전략을 고려해야 한다. 이러한 전략과 관련된 동시성 문제를 관리하기 위해서 분산 동시성 제어가 필요할 수 있다.
위에서 든 이유들이 큰 틀에서는 비슷한 이유이지만, 결론적으로 분산 서버 환경에서 여러 서버가 하나의 데이터베이스에 접근하고 데이터 일관성과 동시성을 보장하기 위해서는 분산 락 또는 다른 분산 동시성 제어 메커니즘을 고려해야 한다.
그리고 이를 비교적 손쉽게 도와주는 Lettuce나 Redisson 를 사용할 수 있다. 둘 모두 Redis를 사용하기 위한 클라이언트 라이브러리인데 Lettuce와 Redisson의 특징은 무엇이고 어떻게 사용될 수 있는지, 그리고 차이는 무엇인지 등 자세히 알아보는 시간을 갖으려고 한다.
1 . Lettuce
Lettuce란 Redis 클라이언트 라이브러리이다. 특징은 다음과 같다.
- Netty를 기반으로 구축되어 있다. 즉 비동기 이벤트 기반 네트워크 프레임워크를 사용하여 구현되어 있으므로, 네트워크 통신에서 발생할 수 있는 블로킹 문제없이 빠른 I/O 처리가 가능하다.
- 동기, 비동기, 리액티브 패턴을 모두 지원한다.
- Redis 클라이언트이므로 당연하게 Redis의 모든 기능을 지원한다.
- 여러 Redis 서버와의 안정적인 통신을 관리하기 위한 광범위한 연결 및 스레딩 옵션을 제공한다.
(Redis 서버를 구축할 때 적절한 장애대응을 위해 한대의 Master와 한대 이상의 Replica 서버로 구성) - Lettuce 클라이언트 인스턴스는 여러 쓰레드에서 공유될 수 있고, 연결 인스턴스가 안전하게 공유된다.
이러한 특징을 갖고 있는 Lettuce를 이용하여 레이스 컨디션을 해결할 수 있다.'SETNX' (Set if not exists) 명령어를 이용하는 방법이 그중 하나이다.Redis에서 'SETNX' 명령어를 사용해 본 결과이다.
127.0.0.1:6379> setnx 1 lock //키1 에 값 lock 설정 시도
(integer) 1 //1이라는 키가 성공적으로 설정되어 성공을 나타내는 integer 1값 반환
127.0.0.1:6379> setnx 1 lock //키1 에 값 lock 설정 다시 시도
(integer) 0 //1이라는 키가 이미 있으므로 설정 실패를 나타내는 integer 0값 반환
127.0.0.1:6379> del 1 //키 1 삭제
(integer) 1 //성공을 나타내는 1값 반환
127.0.0.1:6379> setnx 1 lock //키1 에 값 lock 설정 재시도
(integer) 1 //이전단계에서 키1 을 삭제하였으므로, 1이라는 키가 성공적으로 설정됨
이러한 과정은 레이스 컨디션을 해결하는 시뮬레이션이 될 수 있다. 이 아이디어를 다음과 같이 다시 쓸 수 있다.
- 레이스 컨디션을 해결해야 하는 공유 자원(예: Redis의 키)에 대해 SETNX 명령어를 사용한다.
- 이때, 각 프로세스나 스레드는 자신이 사용하려는 공유 자원의 키를 가지고 SETNX 명령어를 실행한다.
- SETNX 명령어는 해당 키가 존재하지 않을 때만 값을 설정하므로, 여러 프로세스나 스레드가 동시에 SETNX를 실행해도 최종적으로는 한 번만 성공하게 된다.
- SETNX 명령어가 성공하면 해당 프로세스나 스레드는 공유 자원을 안전하게 사용할 수 있다. 그렇지 않은 경우에는 이전에 값을 설정한 다른 프로세스나 스레드가 이미 자원을 확보한 것이므로, 현재 프로세스나 스레드는 다른 처리 방식을 선택할 수 있다.
4번 단계에서와 같이 자원을 사용할 수 없는 경우, 락 획득을 시도하는 프로세스나 스레드가 락을 사용할 수 있는지 반복적으로 확인하면서 락 획득을 시도하는 Spin Lock방식을 사용하여 레이스 컨디션을 해결한다. 이러한 Spin Lock방식에서는 개발자가 retry, timeout 과 같은 기능을 직접 구현해 주어야 한다.
예시코드는 아래와 같다.
Lettuce를 사용한 분산 락 구현
import io.lettuce.core.RedisClient;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class RedisLock {
private RedisClient redisClient;
private StatefulRedisConnection<String, String> connection;
public RedisLock(String redisUrl) {
this.redisClient = RedisClient.create(redisUrl);
this.connection = redisClient.connect();
}
public boolean acquireLock(String lockKey, int timeoutSec, int retryDelay) {
RedisCommands<String, String> commands = connection.sync();
long timeoutMillis = System.currentTimeMillis() + timeoutSec * 1000;
//정해진 타임아웃 시간동안 락 획득 시도
while (System.currentTimeMillis() < timeoutMillis) {
//키가 존재하지 않을 때(nx()) 키 만료시간을 10초로(ex(10)) 설정한 락 획득 시도
String result = commands.set(lockKey, "locked", SetArgs.Builder.nx().ex(10));
if ("OK".equals(result)) {
return true;
}
try {
Thread.sleep(retryDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public void releaseLock(String lockKey) {
connection.sync().del(lockKey);
}
public void closeConnection() {
connection.close();
redisClient.shutdown();
}
}
사용 코드
public class Application {
public static void main(String[] args) {
RedisLock lock = new RedisLock("redis://localhost:6379");
if (lock.acquireLock("myLock", 5, 500)) { // 5초 동안 최대 500ms 간격으로 재시도
try {
// 락 획득 성공
System.out.println("Lock acquired");
// 수행할 작업
} finally {
lock.releaseLock("myLock");
System.out.println("Lock released");
}
} else {
System.out.println("Failed to acquire lock");
}
lock.closeConnection();
}
}
위 구현 코드 중
String result = commands.set(lockKey, "locked", SetArgs.Builder.nx().ex(10));
는 락 획득 시도에 대한 결과를 나타내는 코드이다. SetArgs.Builder.nx().ex(10))를 좀더 자세히 봐보자면
- lockKey: 락을 설정할 키
- "locked": 키에 설정할 값.
- SetArgs.Builder.nx().ex(10): SET 명령의 옵션으로, nx()는 키가 존재하지 않을 때만 값을 설정하도록 지정하고, ex(10)는 설정된 키의 만료 시간을 10초로 설정한다.
ex(10) 코드의 키의 만료 시간 10초 부여는 분산락을 구현할 때 분산 락 메커니즘의 안정성 및 효율성을 향상시키는 중요한 역할을 한다. 자세한 내용은 아래와 같다.
- 데드락 방지 : 분산 시스템에서 데드락은 여러 프로세스가 서로의 자원을 기다리면서 발생할 수 있는 문제이다. ex(10)으로 설정된 만료 시간은 락이 자동으로 해제되도록 함으로써 이러한 데드락 상황을 방지할 수 있다.
- 락의 자동 해제 : 만약 락을 획득한 프로세스가 어떤 이유로든 중단되거나 크래시가 발생해 정상적으로 락을 해제할 수 없는 경우, 설정된 만료 시간이 지나면 락이 자동으로 해제된다. 이는 시스템의 복원력을 높이고, 자원을 영구적으로 점유하는 문제를 예방한다.
- 시스템 안정성 유지 : 분산 시스템의 다양한 컴포넌트 간에 발생할 수 있는 네트워크 지연이나 장애에 대응하기 위해, 만료 시간은 락의 수명을 제어하여 시스템의 안정성을 유지하는 데 도움을 준다. 이는 예상치 못한 상황에서도 시스템이 계속해서 잘 작동할 수 있도록 보장한다.
2. Redisson
Redisson또한 Java 객체를 Redis에 저장하고 쉽게 조작할 수 있게 해주는 Redis 클라이언트이다. 주요 특징은 다음과 같다.
- Map, Set, List, Queue 등 자바 컬렉션 인터페이스를 구현하여, Redis를 백엔드로 사용하는 분산 데이터 구조를 제공한다.
- Lock, Semaphore, CountDownLatch, AtomicLong 등 다양한 동기화 도구를 제공해 분산 환경에서 데이터 동시성을 관리할 수 있다.
- Redis에 저장되는 자바 객체의 직렬화 및 역직렬화 작업을 자동화해준다.
- 비동기, 동기, 리액티브 프로그래밍을 모두 지원한다.
- Redis의 Pub/Sub 기능(발행/구독 모델)을 활용해 이벤트 기반 시스템을 구축할 수 있다.
여러가지 특징이 있지만, 레이스 컨디션 해결을 위해 마지막으로 언급된 Pub/Sub 기능을 이용하여 분산락을 구현해 레이스 컨디션을 해결할 수 있다.
Pub/Sub 을 이용한 분산락 구현의 아이디어는 아래와 같다.
- 프로세스가 락을 요청하면 Redisson은 우선 Redis에 락이 설정되어 있는지 확인한다.
- 락이 이미 획득되어 있는 경우, 요청한 프로세스는 해당 락 해제 이벤트를 기다리기 위해 Pub/Sub 채널을 구독한다.
- 한편 락을 보유한 프로세스가 작업을 마치고 락을 해제하면, Redisson은 Pub/Sub 채널을 통해 이 이벤트를 전달한다.
- 대기 중이던 프로세스가 이 이벤트를 받으면, 락 획득 시도를 재개한다.
구현 코드는 다음과 같다.
1. RLock을 사용한 구현
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonLockExample {
public static void main(String[] args) throws InterruptedException {
// Redisson 클라이언트 설정
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
// 분산 락 획득(Rlock)
RLock lock = redisson.getLock("myLock");
if (lock.tryLock(10, 5, TimeUnit.SECONDS)) {
try {
System.out.println("lock acquired, performing task...");
} finally {
lock.unlock();
System.out.println("lock released.");
}
} else {
System.out.println("unable to acquire lock, task in progress.");
}
redisson.shutdown();
}
}
분산 락을 사용하기 위해 Rlock객체를 사용했으며, 이때 파라미터는 (대기시간, 보유시간, 시간단위) 이다.
즉, 락을 시도하는 대기시간을 설정하여 해당시간 내에 락을 얻을 수 있는지 확인하고
락을 획득했다면 보유시간동안 보유할 수 있게 된다.
이러한 tryLock 메서드는 다음과 같은 장점을 갖는다.
- 무한정 기다리지 않고, 지정된 시간만큼만 대기하기 때문에 데드락 상황을 피할 수 있다.
- leaseTime을 사용하여 락을 자동으로 해제할 수 있어, 예상치 못한 크래시나 오류 상황에서도 다른 프로세스가 락을 얻을 수 있다.
2. RReadWriteLock을 이용한 구현도 가능해 보인다(이 글에서는 생략)
...
위에서 스핀 락(Lettuce)과 Pub/Sub 패턴(Redisson)을 사용하여 레이스 컨디션을 해결할 수 있는 예시 및 특징을 알아보았다. 위 내용을 간략하게 정리한 후 어떤 경우에 어떤 방법을 사용하는 것이 좋은지에 대해 정리하려고 한다.
스핀 락 (Spin Lock)Pub/Sub
특성 | 스핀락 | Pub/Sub |
작동 방식 | 락이 사용 중일 때, 획득할 수 있을 때까지 지속적으로 상태 확인 | 락이 해제될 때까지 이벤트를 구독하여 대기 |
CPU 부하 | 높음, 지속적으로 상태를 확인하므로 CPU 사용률 증가 | 낮음, 이벤트 기반 대기로 불필요한 CPU 사용 없음 |
잠재적 지연 | 없음, 즉시 락을 획득할 수 있음 | 있을 수 있음, 이벤트 수신 및 락 획득에 지연 가능 |
확장성 | 낮음, 여러 스레드가 동시에 상태 확인 시 성능 저하 가능 | 높음, 다수의 스레드가 대기해도 효율적으로 처리 가능 |
복잡성 | 낮음, 구현이 비교적 단순함 | 높음, Pub/Sub 메커니즘 구현이 복잡할 수 있음 |
적합한 사용 사례 | 락이 짧은 시간 내에 해제될 가능성이 높은 경우 | 긴 시간 동안 락이 필요하고, 다수의 스레드가 대기할 경우 |
효율성 | 낮음, 불필요한 CPU 사용 발생 | 높음, 락 해제 이벤트 대기를 통한 효율적 처리 |
- 작업 성격에 따른 선택: 스핀 락은 짧은 시간 내에 락이 해제될 가능성이 높은 경우에 유리하지만, 긴 작업이 필요한 경우는 비효율적이다. Pub/Sub은 작업의 길이에 상관없이 스레드의 자원 낭비를 줄인다.
- 시스템 특성: 스핀 락은 CPU 자원이 충분한 환경에서 더 효과적이지만, CPU 자원이 제한된 환경에서는 Pub/Sub이 더 나은 성능을 제공할 수 있어 보인다.
- 복잡성 및 유지보수: 스핀 락은 구현이 단순하지만 고성능 시스템에서는 Pub/Sub의 효율성을 감안해 더 복잡한 시스템을 선택하는 것이 좋다.
'서버 개발' 카테고리의 다른 글
Spring Boot REST API 회원관리 - (2) Swagger 적용 (0) | 2024.07.30 |
---|---|
Spring Boot REST API 회원관리 - (1) 환경구성 및 "Hello" 출력 (0) | 2024.07.30 |
레이스 컨디션 (Race Condition) -02. Pessimistic Lock, Optimistick Lock (0) | 2024.04.10 |
CURL로 외부 서버와 통신 테스트(+ 겪을 수 있는 에러로그) (0) | 2024.03.25 |
내부망에서 외부 서버와의 통신시 발생할 수 있는 문제들 (0) | 2024.03.20 |