본문 바로가기
프로그래밍/redis

[redis] 트랜잭션(Transaction) - 이론편

by 사바라다 2021. 7. 18.

안녕하세요. 오늘은 redis의 트랜잭션에 대한 이론에 대해서 알아보도록 하겠습니다.

그리고 이후 포스팅에서 실제로 Spring Data Redis를 이용하여 redis의 트랜잭션을 실습해보도록 하겠습니다.

트랜잭션과 Redis

트랜잭션이란 무엇인가요 ? [데이터베이스] 트랜잭션과 격리성 포스팅에서 이전에 저는 트랜잭션에 대해서 별도 포스팅을 한적이 있습니다. 간단하게 말씀드리면 트랜잭션은 나누어지지 않는 최소한의 단위로 만들어 All Or Nothing 전략을 취할 수 있도록 해주는 단위입니다. 즉, 트랜잭션으로 묶게 되면 트랜잭션 내부에서 하나의 로직이 실패하여 오류가 나게되면 모두 취소시키며 그렇지 않으면 모두 성공시키는 것입니다.

Redis는 Key / Value 형태의 NoSQL입니다. [Redis] 캐시(Cache)와 Redis에서 개념을 다룬적이 있습니다. Redis는 일반적으로 인메모리로 이루어져 있습니다. 그래서 캐시의 형태로 많이 사용합니다. 추가적인 특징으로는 다양한 자료구조를 제공해주며 RDS와 AOF 기능으로 영구저장소로도 극소수 사용하기도 합니다.

Redis에서 트랜잭션이라니 조금 어색하다고 생각하실 수 있습니다. 하지만 여러 자료구조를 사용할 수 있는 Redis의 특성상 트랜잭션을 잘 이용하면 더 유용하게 다양한 상황에서 Redis를 사용할 수 있을 것입니다.

Redis 트랜잭션

그렇다면 Redis의 트랜잭션은 어떻게 이용사용할 수 있을까요 ? 트랜잭션을 유지하기 위해서는 순차성을 가져야 하고 도중에 명령어가 치고 들어오지 못하게 Lock이 필요합니다. Redis에서는 MULTI, EXEC, DISCARD 그리고 WATCH 명령어를 이용하면됩니다. 각 명령어에 대한 설명은 아래와 같습니다.

  • MULTI
    • Redis의 트랜잭션을 시작하는 커맨드. 트랜잭션을 시작하면 Redis는 이후 커맨드는 바로 실행되지 않고 queue에 쌓입니다.
  • EXEC
    • 정상적으로 처리되어 queue에 쌓여있는 명령어를 일괄적으로 실행합니다. RDBMS의 Commit과 동일합니다.
  • DISCARD
    • queue에 쌓여있는 명령어를 실괄적으로 폐기합니다. RDMS의 Rollback과 동일합니다.
  • WATCH
    • Redis에서 Lock을 담당하는 명령어입니다. 이 명령어는 낙관적 락(Optimistic Lock) 기반입니다.
    • Watch 명령어를 사용하면 이 후 UNWATCH 되기전에는 1번의 EXEC 또는 Transaction 아닌 다른 커맨드만 허용합니다.

Redis 트랜잭션 기본

그러면 redis CLI를 통해서 테스트해보도록 하겠습니다. 아래는 정상일 경우에 어떻게 동작하는지 알 수 있는 명령어 셋입니다. 먼저 MULTI 커맨드를 입력합니다. 그러면 이제 트랜잭션을 사용할 수 있습니다. 이후에 들어오는 명령어는 바로 실행되는 것이 아니라 큐에 쌓이게("QUEUED") 됩니다. 그리고 마지막에 EXEC 커맨드를 통해 일괄적으로 실행되게 되는 구조입니다. 참고로 GET 커맨드 또한 QUEUED로 쌓이게 됩니다.

>> MULTI
"OK"
>> SET SABARADA BLOG
"QUEUED"
>> SET KAROL BLOG
"QUEUED"
>> GET SABARADA # GET은 어떻게 처리되는지 확인
"QUEUED"
>> EXEC   # QUEUED 된 명령어를 실행한 후 결과를 전부 출력
1) "OK"
2) "OK"
3) "BLOG" # GET 또한 바로 처리되지 않고 QUEUED 된 후 EXEC 했을 때 처리

정상적으로 커맨드가 실행 됬는지 한번 확인해보겠습니다. 트랜잭션 내부의 커맨드가 정상적으로 실행되었기때문에 아래와 같이 GET 했을 때 정상 출력되는 것을 확인할 수 있었습니다.

>> GET SABARADA
"BLOG"
>> GET KAROL
"BLOG"

그렇다면 Rollback은 어떻게 이루어지는지 보도록 하겠습니다. Redis의 Rollback은 RDBMS와 조금 방식이 다릅니다. 아래에서 여러 경우의 예제를 보며 확인해보도록 하겠습니다. MULTI 커맨드를 사용한 후 이용하여 DISCARD 명령어를 명시적으로 실행합니다. 이렇게 한다면 QUEUE에 쌓여있던 명령어가 일괄적으로 없어지게 됩니다.

>> MULTI
"OK"
>> SET SABARADA BLOG
"QUEUED"
>> SET KAROL BLOG
"QUEUED"
>> GET SABARADA
"QUEUED"
>> DISCARD
"OK"

그렇다면 도중에 잘못된 명령어를 사용했을 경우는 어떨가요 ? 아래의 예제는 Transaction 도중에 DD HKD 라고 하는 Redis에는 정의되지 않은 명령어를 실행했을 때를 보여주고 있습니다. 이 경우에는 QUEUE에 쌓였던 모든 명령어가 DISCARD 되는 것을 알 수 있습니다.

>> MULTI
"OK"
>> SET SABARADA 4
"QUEUED"
>> HSET SABARADA 2 3
"QUEUED"
>> DD HKD
(error) unknown command `DD`, with args beginning with: `HKD`, 
>> EXEC
(error) Transaction discarded because of previous errors.

잘못된 자료구조의 명령어를 사용하는건 어떨까요 ? SABARADA key는 Strings 자료구조입니다. HSET은 Map 자료구조에 대한 커맨드이기 때문에 알맞지 않는 자료구조로 인해 오류가 발생합니다. 하지만 아래의 코드를 보시면 일단 정상적으로 QUEUED로 큐에 샇이는 것을 알 수 있습니다. 그리고 EXEC로 트랜잭션을 커밋했습니다. Something went wrong 이라는 에러가 발생한걸 알 수 있습니다.

>> MULTI
"OK"
>> HSET SABARADA 2 3
"QUEUED"
>> SET SABARADA 4
"QUEUED"
>> EXEC
ERROR: Something went wrong.

에러가 발생 했기 때문에 SET 명령어를 실행한 경우도 Rollabck 되어 없어졌을까요 ? 그렇지 않습니다. GET을 해보면 해당 명령어는 잘 적용된 것을 알 수 있습니다. Redis의 트랜잭션은 잘못된 명령어가 하나 있다고 하더라도 정상적으로 사용한 명령어에 대해서는 잘 적용되는 것을 알 수 있었습니다.

>> GET SABARADA
"4"

Redis는 왜 이런 트랜잭션 방법을 택했을까요 ? redis 공식 문서에서는 이러한 이유는 먼저 대부분 개발 과정에서 일어날 수 있는 에러이며 production에서는 거의 발생하지 않는 에러입니다. 또한 rollback을 채택하지 않음으로써 빠른 성능을 유지할 수 있다고 합니다.

Redis 트랜잭션 락

트랜잭션에서 해당 키에 대해서 Lock 또한 중요한 요소입니다. 내가 해당 값을 변경히고 있는데 다른 사람이 동일하게 Key를 건드린다면 잘못 된 값이 입력될 수 있기 때문입니다. 여기에 사용되는 Redis 명령어는 WATCH 입니다. WATCH 명령어를 이용하면 해당 Key는 트랜잭션에서 값 변경을 1번으로 제한할 수 있습니다. 아래는 SABARADA 라는 Key에 WATCH 명령어를 이용하여 Lock을 걸어둔 상태에서 성공하는 커맨드 Set입니다.

>> WATCH SABARADA
"OK"
>> MULTI
"OK"
>> SET SABARADA 3
"QUEUED"
>> EXEC
1) "OK"

아래는 실패하는 커맨드 셋입니다. WATCH를 이용해 KAROL이라는 Key를 Lock을 걸었습니다. 하지만 2번째에서 해당 Key에 대한 값을 바꿔버렸습니다. 이후에 트랜잭션을 이용하여 Karol의 값을 변경합니다. EXEC를 실행할때 WATCH 명령어를 사용한 Key는 CAS(Check And Set)을 내부적으로 돌립니다. 하지만 이 경우에는 이미 KAROL이라는 Key의 값이 변경이 되었기 때문에 Check에서 실패가 되며 값 변경은 실패로 돌아갑니다.

>> WATCH KAROL
"OK"
>> SET KAROL 7
"OK"
>> MULTI
"OK"
>> SET KAROL 5
"QUEUED"
>> EXEC
(nil)

때문에 아래처럼 값을 변경 후 명시적으로 WATCH를 풀어주게 되면 정상적으로 값이 변경 될 수 있다는 것을 알 수 있습니다.

>> WATCH KAROL
"OK"
>> SET KAROL 8
"OK"
>> UNWATCH
"OK"
>> MULTI
"OK"
>> SET KAROL 4
"QUEUED"
>> EXEC 
1) "OK"

추가적인 예제로 아래처럼 Set으로 값을 바꿔주고 트랜잭션을 2번 하게 되면 2번째에서는 성공하게 됩니다. 왜냐하면 EXEC가 호출 될 때 UNWATCH가 묵시적으로 호출되기 때문에 2번째 트랜잭션에 대해서는 해당 키에 대해서는 WATCH가 걸려있지 않기 때문입니다.

>> WATCH KAROL
"OK"
>> SET KAROL 22
"OK"
>> MULTI
"OK"
>> SET KAROL 11
"QUEUED"
>> EXEC
(nil)
>> MULTI
"OK"
>> SET KAORL 11
"QUEUED"
>> EXEC
1) "OK"

마무리

오늘은 이렇게 Redis의 Transaction에 대한 Redis 내부의 명령어를 기준으로 설명드렸습니다.

다음 시간에는 Java와 Spring Data Redis를 이용하여 실습을 해보도록 하겠습니다.

감사합니다.

참조

redis_transactions

댓글