일반적인 Transaction이 필요한 RDB의 경우, 자체적으로 격리 수준을 설정하거나 Rollback과 Commit을 통해 트랜잭션을 구현한다.
하지만 Redis는 어떠한가?
Redis에서 데이터의 일관성과 원자성을 보장받기 위해 주어진 옵션은 그렇게 명확하지 않다.
하나의 Transaction을 격리 시켜야 하며, All or Nothing 원자성을 Redis에서 지켜야 할 때에 도움이 되고자 기록합니다.
Redis에서 여러 Command를 일괄적으로 처리하기 위해 떠올리게 되는 것은 Pipeline일 것이다
Pipeline은 연결된 Redis Client로 여러 개의 커맨드를 한번에 보내고 여러개의 응답을 한번에 받는 것을 가능하도록 한다
다만 일반적인 파이프라인이 트랜잭션이 보장된다고 할 수 없다. 즉, 파이프라인 실행 도중 해당 데이터에 대하여 다른 커맨드로 인해 변경될 수 있으며 데이터의 일관성에 문제가 발생할 수 있다
네트워크 지연
: Redis Pipeline을 사용하면 여러 개의 커맨드를 한 번에 보내고, 한 번에 여러 개의 응답을 받는 것이 가능합니다. 그러나 네트워크 지연으로 인해 커맨드가 서버에 도착하는 순서와 응답이 도착하는 순서가 일치하지 않을 수 있습니다. 따라서 응답이 먼저 도착하는 경우에도, 실제로는 나중에 실행한 커맨드의 결과일 수 있습니다.다중 스레드 환경
: Redis 서버는 다중 스레드로 동작하며, 여러 클라이언트가 동시에 요청을 보낼 수 있습니다. 따라서 여러 클라이언트가 동시에 Pipeline을 사용할 경우, 커맨드의 실행 순서가 보장되지 않을 수 있습니다.Redis 서버 설정
: Redis 서버의 설정에 따라 일관성이 달라질 수 있습니다. 예를 들어, Redis 서버가 "slaveof" 설정을 사용하여 데이터를 레플리케이션하는 경우, 일관성이 보장되지 않을 수 있습니다.이를 해결하는 것이 TxPipeline이다
트랜잭션 보장
: TxPipeline을 사용하면 일괄 처리된 커맨드는 하나의 트랜잭션으로 간주되어, 실행 중 다른 커맨드가 중간에 끼어들지 않습니다. 이로써 데이터의 일관성을 보장합니다.성능 향상
: 여러 개의 커맨드를 한 번에 보내므로 네트워크 오버헤드를 줄일 수 있어 성능이 향상될 수 있습니다.원자성 보장
: TxPipeline 내에서 실행되는 모든 커맨드는 성공하거나 실패되며, 롤백은 지원하지 않습니다.메모리 사용
: 모든 커맨드와 결과가 메모리에 저장되므로, 큰 트랜잭션을 처리할 때 메모리 사용량이 증가할 수 있습니다.복잡성
: TxPipeline은 다른 파이프라인과 혼합해서 사용할 때 주의가 필요하며, 트랜잭션의 범위를 신중하게 관리해야 합니다.sequenceDiagram
participant 클라이언트
participant 레디스
Note over 클라이언트,레디스: 트랜잭션 시작 전
클라이언트->>레디스: WATCH 키
Note over 레디스: 키 감시 시작
클라이언트->>레디스: MULTI
Note over 레디스: 트랜잭션 시작
클라이언트->>레디스: 명령1
클라이언트->>레디스: 명령2
클라이언트->>레디스: 명령3
Note over 클라이언트: 다른 명령 추가 가능
alt 모든 명령 성공
클라이언트->>레디스: EXEC
Note over 레디스: 트랜잭션 실행
레디스-->>클라이언트: 성공 응답
else 하나 이상의 명령 실패
클라이언트->>레디스: DISCARD
Note over 레디스: 트랜잭션 취소
레디스-->>클라이언트: 실패 응답
end
WATCH를 통해 특정 키의 변경을 감시하고, 이를 통해 트랜잭션의 일관성을 보장할 수 있습니다.
만약 WATCH 중에 감시된 키가 다른 클라이언트에 의해 변경되면, 해당 트랜잭션은 실패
ex. TxPipeline 도중 에러 발생 시나리오
MULTI
SET key1 value1
SET key2 value2
SET key3 value3
EXEC
시나리오 : SET key2 value2 커맨드가 실패
"OOM command not allowed when used memory > 'maxmemory'"
"WRONGTYPE Operation against a key holding the wrong kind of value"
일반적으로 일어날 수 있는 경우가 드무며, 가장 예상 가능한 시나리오는 Redis Client Connection이 끊어진 케이스가 될것이다.
만약 Client Connection이 끊어진 경우, 실패한 키에 대하여 롤백을 수동으로 진행한다 하여도 롤백 자체가 수행되지 않기 때문에 완벽한 해답은 되지 않는다
즉, TxPipeline은 일관성은 보장 되지만 완벽한 원자성은 보장되지 않는다
Lua 스크립트 내에서 여러 Redis 커맨드를 실행하고, 이를 원자적으로 실행할 수 있다
경량 및 빠른 실행
: Lua는 경량 스크립팅 언어로, 실행 시스템 자원을 적게 소모하며 빠르게 실행됩니다. 이러한 특징은 임베디드 시스템이나 게임 엔진 등 리소스가 제한된 환경에서 유용합니다.내장 스크립트 언어
: Lua는 많은 애플리케이션과 게임 엔진에서 내장 스크립트 언어로 사용됩니다. 이는 애플리케이션의 확장성을 높이고 사용자 정의 스크립트를 통해 기능을 확장하기에 용이합니다.가독성과 간결성
: Lua는 간결하고 가독성이 높은 문법을 가지고 있어 쉽게 이해하고 작성할 수 있습니다.결국 Lua 또한 트랜잭션에 대한 롤백은 지원하지 않습니다.
기본적으로 삽입 요청 시, Validation을 체크하는 것을 고려할 때에 일어날 수 있는 최악의 시나리오는 네트워크의 단절이며 해당 방법은 어떠한 방법으로도 복구 될 수 없습니다.
Redis Pipeline에 저장된 커맨드가 원자적으로 한번에 실행 될 수 없기 때문입니다
테스트 시나리오
key1 ~ key5 까지 세팅하는 도중 에러 발생 시, 롤백 테스트
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 서버 주소
})
luaScript := `
for i = 1, #KEYS do
if KEYS[i] == 'key3' then
error('key3 설정 시 에러 발생')
else
redis.call('SET', KEYS[i], ARGV[i])
end
end
`
keys := []string{"key1", "key2", "key3", "key4", "key5"}
values := []interface{}{"value1", "value2", "value3", "value4", "value5"}
err := rdb.Eval(ctx, luaScript, keys, values...).Err()
if err != nil {
fmt.Printf("Lua 스크립트 실행 중 오류 발생: %v\n", err)
return
}
fmt.Println("Lua 스크립트 실행 완료")
}
[결과]
TxPipeline | Lua Script | |
---|---|---|
특징 | - Redis Pipeline과 동일한 코드로 수행 가능 |
벤치마크
Redis에 1,000개 Key 세팅 기준
항목 | 테스트 횟수 | 평균 실행 시간 (ms) |
---|---|---|
Lua 스크립트 | 1424 | 0.83 |
TxPipeline | 460 | 2.56 |
Pipeline | 506 | 2.34 |