-
Notifications
You must be signed in to change notification settings - Fork 0
Redis Transaction
Redis 의 트랜잭션은 여러 명령어를 하나의 작업 단위로 묶어 실행할 수 있게 해준다. 이를 위해 주요 명령어로는 MULTI, EXEC, DISCARD, WATCH 이다.
- 또 다른 클라이언트의 요청이 트랜잭션 중에 간섭할 수 없다. → 명령어들이 하나의 명령어처럼 실행됨을 보장한다.
- EXEC 명령어는 트랜잭션의 모든 명령어를 실행한다. 클라이언트의 연결이 EXEC 명령 실행 전에 끊긴다면 명령어는 실행이 안된다. 트랜잭션의 명령어들은 즉시 실행되는 것이 아니라 큐에 보관되었다가 EXEC 명령어가 호출되면 큐에 저장된 모든 명령어들을 순차적으로 실행한다.
- 디스크 쓰기 작업의 측면에서도 원자성을 보장하는데 AOF(Append Only File모드) 일 때 단일 write 시스템 콜로 디스크에 기록한다.하나의 write 시스템 콜로 쓰기 때문에 모두 디스크에 기록되거나 모두 기록되지 않는 등 원자성이 지켜진다.
-
트랜잭션 시작(MULTI)
클라이언트가 MULTI 명령어를 보내면, Redis 는 트랜잭션 모드에 진입을 하고, 모든 후속 명령어들을 바로 실행하는 대신 내부 큐에 저장이 된다.
이 때, Redis 는 항상 OK 라는 응답을 보내서 트랜잭션 모드가 시작되었음을 알린다.
-
명령어 큐잉(QUEUED)
MULTI 이후에 실행되는 모든 명령어들은 실제 실행되지 않고 큐에 저장이 된다.
QUEUED 라는 응답이 오면 이는 명령어가 성공적으로 큐에 저장되었음을 의미한다.
-
트랜잭션 실행 (EXEC)
클라이언트가 EXEC 명령어를 호출하면, 큐에 저장된 모든 명령어들이 순서대로 실행된다.
실행 결과는 배열 형태로 반환되며, 배열의 각 요소는 트랜잭션 내에서 실행된 각 명령어의 결과를 나타낸다.
-
트랜잭션 취소 (DISCARD)
만약 트랜잭션을 실행하지 않고 취소하고 싶다면, DISCARD 명령어를 사용할 수 있다.
이 명령어를 호출하면 내부 큐에 저장된 모든 명령어가 삭제되고, 트랜잭션 모드에서 벗어나게 된다.
-
EXEC 호출 전 에러 (명령어 큐잉 시점의 에러)
-
문법 오류 등:
명령어의 인자 수가 잘못되었거나, 잘못된 명령어를 사용한 경우, 해당 명령은 큐에 추가되지 않고 즉시 에러를 반환한다.
-
심각한 조건
메모리 부족과 같이 서버에 심각한 문제가 발생한 경우에도 큐잉 시 에러가 발생할 수 있다.
-
Redis 2.6.5 이상 동작 방식
서버는 명령어들이 큐에 쌓이는 동안 에러를 감지하면, 이후 EXEC 호출 시 전체 트랜잭션을 실행하지 않고 에러를 반환하며 트랜잭션을 취소한다.
-
Redis 2.6.5 미만 동작 방식
클라이언트가 각 명령어가 QUEUED 응답을 받았는지 직접 확인해야 한다. 만약 어떤 명령어에서 에러가 발생했다면, 일반적으로 클라이언트는 트랜잭션을 취소한다.
-
-
EXEC 호출 후 에러 (명령어 실행 시점의 에러)
트랜잭션 내에 모든 명령어가 큐에 성공적으로 추가되고 EXEC가 호출되면, Redis는 모든 명령어를 순차적으로 실행한다. 이 과정에서 하나의 명령어가 에러가 발생해도 나머지 명령어들은 정상적으로 실행이 된다.
Redis는 롤백 기능을 제공하지 않고 대신 트랜잭션 내 명령어를 순차적, 원자적으로 실행하여 간단하고 빠른 성능을 유지하는 설계를 했다.
DISCARD 명령어는 트랜잭션 큐를 비워서, 큐에 있는 모든 명령어들을 실행하지 않고 트랜잭션을 종료하는 역할을 한다.
다음 파트가 낙관적 락 파트에 대한 설명인데 나의 레디스 개념과 충돌되는 설명이 있었다. 레디스는 싱글 쓰레드이기 때문에 동시성 문제가 발생하지 않지 않을까 생각을 했으나 낙관적 락이라는 개념이 있다는 것은 동시성 이슈가 있다는 말이다!!!
낙관적 락에 대한 설명에 들어가기 앞서 레디스 큐에 대한 설명을 해야 겠다.
레디스는 명령어를 담고 있는 글로벌 큐가 있다. 이 명령어들은 EXEC 한 명령어들이고 순차적으로 실행이 된다. 앞에서 설명한 트랜잭션에서의 큐는 글로벌 큐와 다른 로컬 큐이다. 클라이언트가 트랜잭션을 시작하면 로컬 큐에 명령어를 보관하고 있다가 EXEC 하면 글로벌 큐로 명령어들이 들어간다. 아, 이래서 동시성 이슈가 발생할 수 있는 것이었다.
클라이언트 두 명이 같은 값을 각각 조회하고 조회한 값에서 1을 증가시킨다면 2가 증가되지 않고 1만 증가되게 될 것이다.
WATCH 명령어를 사용하여 구현된다.
- WATCH 명령어의 역할
- WATCH를 사용하면 특정 키들을 모니터링할 수 있다.
- WATCH 된 키들 중 하나라도 EXEC 명령어가 실행되기 전에 변경되면, 전체 트랜잭션은 자동으로 중단되고 EXEC은 NULL을 반환한다.
- 이는 다른 클라이어트가 해당 키를 수정했음을 감지하여, 트랜잭션 실행 전 데이터의 변경을 확인하는 역할을 한다.
- 동시성 문제 해결
- 여러 클라이언트가 동시에 mykey 의 값을 1씩 증가시키려 할 때, 단순하게 GET 후 SET 방식으로 처리하면 레이스 컨디션이 발생할 수 있다. WATCH 를 사용하면 한 클라이언트가 값을 읽은 후, 다른 클라이언트의 값을 수정했을 경우 트랜잭션이 실패하므로, 클라이언트는 트랜잭션을 재시도할 수 있다. 이를 통해 여러 클라이언트가 동시에 작업하더라도 데이터의 일관성을 유지할 수 있다.
- 이 때 재시도는 로컬 큐에 있는 모든 작업이다. 왜냐하면, EXEC 의 결과의 대상은 단일 명령어가 아닌 트랜잭션에 속해 있는 모든 명령어기 때문이다.
WATCH 명령어는 특정 키를 감시하여, 해당 키들이 변경되지 않은 경우에만 이후에 실행되는 트랜잭션을 실행하도록 만든다. WATCH 된 키 중 하나라도 WATCH 후부터 EXEC 가 호출되기 전까지 변경이 발생하면 해당 트랜잭션을 취소하고 Null을 반환한다.
WATCH 가 감시하는 변경
- 다른 클라이언트가 수행하는 쓰기 작업
- 같은 클라이언트가 WATCH 이후에 실행한 쓰기 작업
여러 WATCH 호출과 UNWATCH
- WATCH 는 여러 번 호출할 수 있으며, 여러 WATCH 호출은 누적되어 효과를 발휘한다.
- EXEC가 호출되면, 성공 여부와 관계없이 모든 WATCH된 키들이 자동으로 UNWATCH 처리된다.
- 클라이언트 연결이 종료될 경우에도 모든 WATCH 가 해제된다.