본문 바로가기
Redis

Redis 다중 명령 원자성 보장 방법

by sangyunpark99 2025. 4. 23.

Redis는 싱글 스레드 기반의 구조 덕분에 단일 명령어는 원자성을 보장합니다. 하지만 여러 개의 명령어를 연속으로 실행할 경우, 명령 사이에 다른 클라이언트의 명령이 끼어들 수 있어 경합이 발생합니다. 이 문제를 방지하고자 Redis는 다음 세 가지 방법으로 다중 명령에 대한 원자성을 제공합니다.

  • Redis Transaction
  • 낙관적 락(WATCH)
  • Lua Script

예를 들어, 다음과 같은 코드가 있다고 가정합니다.

Long stock = redisTemplate.opsForValue().get("stock:1");
if(stock > 0) {
    redisTemplate.opsForValue().decrement("stock:1");
}

 

이 코드는 2개의 명령(GET, DECR)로 구성되어 있습니다. 만약 이 두 명령어 사이에 다른 클라이언트가 끼어들게 되는 경우, 동시성 문제가 발생할 수 있습니다. 이를 방지하기 위해선 여러 명령을 하나의 원자적 작업으로 묶는 방법이 필요합니다.

 

Redis Transaction

 

Redis는 MULTI와 EXEC 명령을 통해 여러 명령을 트랜잭션처럼 묶어 실행할 수 있습니다.

127.0.0.1:6379> SET stock 11
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECR stock
QUEUED
127.0.0.1:6379(TX)> SET order:123 confirmed
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 10
2) OK

 

이때 명령어들은 QUEUED 상태로 대기하며, EXEC이 호출되면 순차적으로 실행됩니다.

 

트랜잭션 구성 명령어는 아래와 같습니다.

  • MULTI : 트랜잭션 시작
  • EXEC : 큐에 쌓인 명령 실행
  • DISCARD : 트랜잭션 취소
  • WATCH : 낙관적 락 구현시 사용
  • UNWATCH : WATCH를 취소

 

트랜잭션 사용시 보장되는 특징은 아래와 같습니다.

  • 트랜잭션 실행 중 외부 명령의 개입이 차단됩니다.(명령 단위 격리 보장)
  • EXEC이 호출되지 않으면 아무 명령도 실행되지 않습니다.

 

트랜잭션 사용시 주의할 점은 다음과 같습니다.

  • 문법 오류가 포함된 명령이 MULTI 이후 들어가면 EXEC시 전체 트랜잭션이 무효화됩니다.
  • EXEC 이후 실행 중 하나의 명령에서 실패해도, 나머지는 그대로 실행되며 롤백 되지 않습니다.

 

낙관적 락

RDBMS에서 자주 사용하는 Compare-And-Swap(CAS) 개념을 Redis에서도 WATCH 명령어를 통해 구현할 수 있습니다.

RDBMS에서 CAS (Compare-And-Swap)는 동시성 환경에서 데이터를 안전하게 업데이트하는 데 사용되는 원자적 연산입니다. 특정 메모리 위치의 값을 비교하여 예상하는 값과 같으면 새로운 값으로 교체하는 방식입니다

 

아래와 같이 WATCH 명령어를 사용할 경우 낙관적 락을 구현할 수 있습니다.

127.0.0.1:6379> SET stock 10
OK
127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECR stock
QUEUED

127.0.0.1:6379> SET stock 5 // 다른 터미널 창에서 해당 명령어 입력

127.0.0.1:6379(TX)> EXEC
(nil)

 

WATCH된 키가 EXEC 전에 다른 클라이언트에 의해 변경되면 트랜잭션은 중단됩니다.(EXEC 결과 null 반환)

 

Lua Script를 이용한 원자성 보장

EVAL 명령을 사용하여 Redis 서버에서 Lua 스크립트를 실행할 수 있고, 이 방식은 다음과 같은 장점을 가집니다.

  • 서버 측에서 실행하므로 클라이언트와 서버간의 네트워크 왕복(RTT)이 제거됩니다.
  • 스크립트 전체가 하나의 명령처럼 실행되므로, 원자성이 보장됩니다.
  • Redis가 기본으로 제공하지 않는 복합 로직 구현이 가능해집니다.

복합 로직을 구현한 예시 코드는 아래와 같습니다.

EVAL "if redis.call('get', KEYS[1]) >= ARGV[1] then return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end" 1 stock 2

 

재고 차감 로직을 하나의 Lua 스크립트로 작성해서 조건 검사와 재고 차감을 원자적으로 실행합니다.

 

RTT(Round-Trip Time)은 네트워크 용어로, 요청을 보낸 후 응답을 받기까지 걸리는 시간을 의미합니다.
즉, 클라이언트가 서버에 요청 패킷을 보내고, 서버가 그에 대한 응답 패킷을 다시 클라이언트로 보낼 때까지의 시간을 말합니다.

 

참고 : Redis Pipelining(원자성 보장x)

Pipelining은 성능 향상을 위해 여러 명령을 서버로 한번에 전송하는 기능입니다. 하지만, 이는 원자성을 전혀 보장하지 않습니다.

SET a 1
SET b 2
SET c 3

 

이 명령을 파이프라인으로 묶어 전송하면 성능은 좋아지지만, 다른 클라이언트 명령이 끼어들 수 있어 일관성 문제가 발생합니다..

즉, 파이프라인은 트랜잭션이나 Lua와는 목적이 다르고, RTT 최적화 용도로만 사용하는 것이 좋습니다.

 

파이프라이닝을 사용하면 여러 명령을 한꺼번에 보내고, 응답도 한번에 받기 때문에 개별 RTT를 줄이진 못해도 전체적인 지연 시간을 크게 줄일 수 있습니다.

 

상황별 비교

목적 방식 설명
단순한 SET/GET 연산 단일 Redis 명령 Redis는 기본적으로 모든 단일 명령이 원자성을 보장하므로 별도 트랜잭션 없이 바로 사용 가능
조건부 연산 Lua Script 조건 판단 + 변경 연산을 원자적으로 묶어야 하는 경우
트랜잭션은 중간 커맨드 실행 실패에 취약하므로 Lua로 단일 스크립트로 작성해 원자성을 확보
복수 키를 한꺼번에 처리 Redis Transaction 여러 키의 값을 동시에 초기화하거나 증가시키는 경우
단, 트랜잭션은 실행 순서 보장에는 유리하지만, 중간 실패에 대한 롤백이 불가. 따라서 결과에 민감하지 않은 일괄 처리 성격의 작업에 적합
경쟁이 높은 환경에서의 키 보호 WATCH + MULTI + EXEC 유저 포인트 차감처럼 동시에 여러 클라이언트가 동일 키에 접근할 수 있는 환경에 사용
성능 개선만 필요할 때 Pipelining 명령 실행 순서나 원자성에 신경 쓰지 않고, 다수의 요청을 묶어 네트워크 왕복(RTT)을 줄이기 위한 용도로 사용. 단, 성능 최적화 전용이며 동시성 보호는 안됨

 

 

'Redis' 카테고리의 다른 글

Redis로 동시성 제어하기  (0) 2025.02.24
Redis SortedSet 정말 빠른가?  (0) 2024.10.17
Redis, 캐싱하면 정말 응답 속도가 빨라?  (0) 2024.10.01