공식문서로 알아보는 Redis Transaction
개요
redis에서 multi, exec, discard, watch 등의 command를 공부하다 보니 redis transaction이란 개념이 존재한다는 것을 알게 되었습니다.
NoSQL은 트랜잭션이 없다고 알고 있었는데, 트랜잭션에 대한 니즈가 생기면서 RDBMS보다 약한 수준으로 지원한다고 합니다.
Redis 공식문서를 기반으로 redis transaction에 대해 알아보고자 합니다.
Redis Transaction이란?
redis의 명령 중 MULTI, EXEC, DISCARD, WATCH를 활용하여 트랜잭션을 사용할 수 있습니다.
Redis 2.2 버전 이상부터는 Redis의 트랜잭션은 2가지를 보장해 줍니다.
- 모든 명령이 serialized 되어 순차적으로 실행됩니다, 다른 클라이언트가 보낸 요청은 Redis 트랜잭션이 실행되는 도중에 처리되지 않습니다.
- exec 명령은 모든 실행을 트리거하며, 해당 명령이 호출될 때 모든 작업이 수행됩니다. append-only-file을 사용하고 있다면 디스크에 트랜잭션을 쓰기 위해 single write(2) syscall을 사용해야 합니다. 이때 redis server가 죽어버리면 일부 작업만 수행될 수 있습니다. Redis는 재시작 시 이 상태를 감지하고 오류와 함께 종료되며, redis-check-aof tool을 사용하여 append-only-file을 수정하여 부분 트랜잭션을 제거할 수 있습니다.
사용법
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
MULTI 명령을 사용하여 Redis 트랜잭션을 입력할 수 있고 항상 OK로 응답합니다.
이렇게 되면 해당 명령을 바로 실행하지 않고 큐에 대기시킵니다.
위에서 보았듯이 EXEC가 호출되면 모든 명령이 실행됩니다.
만약 EXEC대신 DISCARD를 호출한다면 트랜잭션 대기열이 플러시 되고 트랜잭션이 종료됩니다.
트랜잭션도중 예외가 발생하면 어떻게 될까?
발생할 수 있는 2가지 종류의 예외
- 첫 번째: 명령들을 queue에 넣지 못하여 EXEC가 호출되기 전 오류 발생하는 경우 (메모리 부족, 잘못된 명령어 이름 등)
Redis 2.6.5 버전부터는 명령이 누적되는 동안 오류를 감지하고 EXEC를 수행할 때 오류를 반환하는 트랜잭션의 실행을 거부하고 삭제합니다.
Redis 2.6.5 이전 버전에서는 클라이언트가 큐에 대기 중인 명령들을 확인하여 이전에 오류를 감지했어야 합니다.
- 두 번째: EXEC가 호출된 후 명령이 실패하는 경우 (예를 들어 String Value에 list operation을 수행)
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-WRONGTYPE Operation against a key holding the wrong kind of value
MULTI로 트랜잭션을 시작하고, SET LPOP operation을 큐에 저장합니다.
이후에 실행을 하면 2개의 명령어의 응답에 대해 하나는 OK이며 하나는 에러 응답입니다.
사용자에게 어떻게 오류를 제공할지는 클라이언트의 몫입니다.
트랜잭션 중에 일부 명령이 실패해도 다른 모든 명령이 실행됩니다.
한마디로 롤백이 수행되지 않습니다.
왜 Redis Transaction은 롤백을 지원하지 않을까?
Redis의 단순성과 성능에 큰 영향을 미치기 때문에 롤백을 지원하지 않습니다.
Redis 트랜잭션을 활용하게되면 Queue에 명령어를 쌓아두고 마지막의 EXEC 커맨드를 통해 일괄적으로 실행하게 됩니다.
redis CLI를 통해 트랜잭션에 대해 조금 더 알아보겠습니다.
정상적으로 key에 value들이 저장되고 조회가 발생한 트랜잭션의 예시입니다.
>> MULTI # 트랜잭션의 시작을 명시
"OK"
>> SET junwoo1 test # junwoo1이라는 key에 test라는 value를 저장
"QUEUED"
>> SET junwoo2 test
"QUEUED"
>> GET junwoo1 # GET은 어떻게 처리되는지 확인
"QUEUED"
>> EXEC # QUEUED 된 명령어를 실행한 후 결과를 전부 출력
1) "OK"
2) "OK"
3) "test" # GET 또한 바로 처리되지 않고 QUEUED 된 후 EXEC 했을 때 처리
각 메시지는 큐에 저장(QUEUED)되고 exec 명령어와 동시에 실행됩니다.
중간에 에러가 발생하면 어떻게 될까요?
일반적인 RDB의 트랜잭션을 생각한다면 롤백이 발생해야 하지만 SET 명령어가 반영되고 GET으로 조회되는것을 알 수 있습니다.
Redis의 트랜잭션은 일부분 갱신(Particial Update)가 발생할 수 있으며 큐에 쌓아 일괄적으로 명령을 수행한다는 의의만 있습니다.
(ACID를 보장하진 않으며 독립적인 작업단위의 정도)
CAS(check-and-set)을 활용한 Optimistic Locking
WATCH 명령어를 사용해서 Redis transaction에 CAS 동작을 구현할 수 있습니다.
WATCH 된 키는 해당 키에 대한 변경을 감지합니다.
EXEC 명령 전에 하나 이상의 WATCHED 키가 수정되면 전체 트랜잭션이 중단되고 EXEC는 트랜잭션이 실패했음을 알리기 위해 NULL 응답을 반환합니다.
예를 들어 Redis에 INCR이 없다고 가정하고 해 보겠습니다
INCR은 멀티스레드 환경에서 atomic 하게 key의 값을 1 증가시켜 주는 명령어입니다.
val = GET mykey
val = val + 1
SET mykey $val
위와 같은 방식으로 +1을 시키려고 하지만 멀티스레드환경에서 안전하지 않을 수 있습니다.
예를 들어 클라이언트 A, B가 존재하고 동시에 10을 읽고 11로 업데이트를 한다면 기대했던 12가 나오지 않습니다.
이때 Watch를 활용할 수 있습니다.
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
WATCH 호출과 EXEC 호출사이에 val의 결과를 수정하면 트랜잭션이 실패합니다.
단지 새로운 경합(race)이 발생하지 않도록 작업을 재수행하면되고 이런 잠금을 낙관적 잠금 = Optimistic locking이라 합니다.
WATCH는 EXEC를 조건부로 만드는 명령어입니다.
즉, WATCH 된 키가 수정되지 않은 경우에만 트랜잭션을 수행하도록 Redis에 요청합니다.
수정에는 클라이언트의 쓰기 명령 혹은 ttl expire 및 eviection이 있습니다.
EXEC가 호출되는 순간 모든 키가 UNWATCH 됩니다.
또는 클라이언트 연결이 끊어지면 모든 키가 UNWATCH 됩니다.
argument 없이 UNWATCH 명령어를 사용하면 모든 키를 UNWATCH 시킬 수 있습니다.
Redis 6.0.9 이전 버전에서는 만료된 키로 인해 트랜잭션이 중단되지 않습니다.
해당 Issue는 Redis 프로젝트의 Github에 올라와있고 merge 되어 해결되었습니다.
https://github.com/redis/redis/pull/7920
Redis scripting and transactions
redis에서 트랜잭션 작업을 수행할 때 redis scripts를 고려해 볼 수 있습니다.
일반적으로 script가 더 빠르고 간단합니다.
https://redis.io/commands/eval/
EVAL은 Lua 스크립트를 실행할 수 있는 강력하고 유연한 기능입니다.
이 기능을 사용하면 단일 Redis 명령으로는 수행할 수 없는 복잡한 작업이나 일련의 명령을 실행할 수 있습니다.
특히 원자 연산을 수행하고, 왕복 지연 시간을 줄이고, 복잡한 데이터 조작 로직을 생성하는 데 유용합니다.
참고자료
https://redis.io/docs/interact/transactions/
https://sabarada.tistory.com/177