[Redis] Redisson을 이용한 분산 시스템에서의 락 구현
Redis를 사용하다가 갑자기 분산락에 대해 공부하게 되었다. 관련해 궁금했던 점들을 정리한다.
분산 시스템에서 락(Lock)은 여러 프로세스나 스레드가 동시에 동일한 자원에 접근하는 것을 제어하기 위해 필수적이다. 특히 Redis는 단순하면서도 빠른 성능을 제공하는 인메모리 데이터 스토어로, 분산 락을 구현하는 데 자주 사용된다.
Redis의 싱글 스레드 특성 및 Atomic 연산
Redis는 싱글 스레드 기반으로 동작하기 때문에, 한 번에 하나의 작업만 처리하게 된다. 이를 통해 자연스럽게 명령이 순차적으로 실행되며, 이를 기반으로 원자성(atomicity)을 보장한다. 즉, 하나의 명령이 실행되는 동안 다른 명령이 끼어들 수 없어서 락 구현이 상대적으로 간단해진다.
Redis는 트랜잭션과 Lua 스크립트를 통해 atomic 연산을 지원한다. 트랜잭션은 여러 명령어를 묶어서 실행할 수 있지만, 각각의 명령어가 독립적으로 실행되기 때문에 트랜잭션 내에서 명령어의 결과를 즉시 활용하는 것은 어렵다. 반면, Lua 스크립트는 명령어의 결과를 바로 다음 명령어에서 사용할 수 있는 장점이 있어 더 복잡한 원자적 연산을 구현할 때 많이 사용된다.
트랜잭션의 한계와 Lua 스크립트의 강점
Redis 트랜잭션은 MULTI
명령어를 사용해 여러 명령어를 모아 EXEC
명령어로 실행한다. 그러나 이 방법은 각각의 명령어가 독립적으로 실행되기 때문에, 명령어 사이에서 상호작용하는 복잡한 연산을 수행하는 데는 한계가 있다.
예를 들어, INCR
로 값을 증가시킨 후 그 값을 바로 다음 명령에서 사용하는 것은 트랜잭션 내에서 불가능하다. 그러나 Lua 스크립트를 사용하면 이를 쉽게 해결할 수 있다. Lua 스크립트는 Redis의 여러 명령을 하나의 연산으로 처리해, 그 결과를 즉시 다른 연산에 활용할 수 있게 해준다.
local value = redis.call('INCR', 'key1') -- key1을 증가시키고 그 결과를 변수에 저장
redis.call('SET', 'key2', value) -- 그 결과값을 key2에 저장
이 스크립트는 단일 연산으로 실행되어, 락의 원자성을 보장하면서 복잡한 연산을 수행할 수 있다.
Redis를 이용한 락 구현: SETNX와 스핀락
분산 락을 구현할 때 가장 기본적인 방법은 Redis의 SETNX
(Set if Not Exists) 명령어를 사용하는 것이다. SETNX
는 해당 키가 존재하지 않을 때만 값을 설정하고, 존재하면 설정을 실패한다. 이를 이용해 하나의 프로세스만 자원에 접근할 수 있도록 락을 구현할 수 있다.
그러나 락을 얻지 못한 경우에 계속해서 시도하는 스핀락(Spin Lock) 방식은 주의가 필요하다. 스핀락은 락을 얻을 때까지 반복적으로 시도하며, 실패할 경우 일정 시간 동안 대기 후 다시 시도하는 방식이다. 이 방법은 단순하지만, Redis 서버에 과부하를 유발할 수 있다.
Redisson을 통한 효율적인 락 구현
Redisson은 Redis 클라이언트 라이브러리 중 하나로, Redis의 분산 락을 더 효율적으로 관리할 수 있는 방법을 제공한다. Redisson은 단순한 스핀락 방식 대신 Redis의 Pub/Sub(Publish/Subscribe) 기능을 활용해 락을 처리한다.
Redisson은 먼저 락을 즉시 얻으려 시도하고, 성공하면 바로 반환한다. 만약 실패하면 Pub/Sub 채널을 통해 락이 해제될 때까지 대기한다. 락이 해제되면, 다시 락을 얻으려 시도하고, 이 과정이 반복된다. 이 방식은 불필요한 반복 시도를 줄이고, Redis와 애플리케이션에 부하를 최소화한다.
1. 락을 시도하여 성공하면 바로 반환한다.
2. 실패하면 Pub/Sub 채널을 통해 락 해제 이벤트를 기다린다.
3. 락이 해제되었다는 메시지가 오면 다시 락을 시도한다.
4. 주어진 타임아웃까지 반복하고, 실패 시 false를 반환한다.
Pub/Sub의 역할
Redisson이 락을 관리할 때 사용하는 Pub/Sub은 Redis의 메시징 시스템이다. Redisson은 락을 해제할 때 Redis에서 PUBLISH
명령을 사용해 특정 채널에 메시지를 보낸다. 이 메시지를 구독한 클라이언트는 락이 해제되었음을 인지하고, 다시 락을 시도한다.
이 방식은 Redis 서버에 락을 계속해서 요청하는 폴링(polling) 방식보다 훨씬 효율적이다. 클라이언트가 Redis에 지속적으로 요청을 보내지 않고, 메시지를 기다리며 대기하기 때문에 Redis와 애플리케이션의 부하를 크게 줄일 수 있다.
결론
Redis는 싱글 스레드 특성 덕분에 atomic한 연산을 쉽게 구현할 수 있으며, SETNX
나 Lua 스크립트를 활용해 분산 락을 구현할 수 있다. 그러나 스핀락 방식은 Redis 서버에 부하를 줄 수 있기 때문에, Pub/Sub을 활용한 Redisson과 같은 방법을 사용하면 더 효율적으로 락을 관리할 수 있다.