결론
- Redis Transaction 내에서 중간 값을 읽어서 사용할 수 없습니다. MULTI 이후 모든 명령어는 즉시 실행되지 않고 큐에 저장되며, "QUEUED" 문자열만 반환합니다. 실제 결과는 EXEC 호출 후에만 받을 수 있습니다. Redis 공식 문서 - Transactions (opens in a new tab)
Redis 공식 문서에서 "all commands will reply with the string QUEUED" 라고 명시되어 있음.
- Pipeline과 Transaction의 핵심 차이는 원자성 보장 여부입니다. Pipeline은 네트워크 최적화만 제공하고 다른 클라이언트의 명령어가 끼어들 수 있지만, Transaction은 모든 명령어가 순차적으로 중단 없이 실행됨을 보장합니다. Redis 공식 문서 - Pipelines and transactions (opens in a new tab)
Redis 공식 문서에서 "Transactions guarantee that all the included commands will execute to completion without being interrupted by commands from other clients" 라고 명시되어 있음.
질문 1: Transaction에서 중간 값을 읽어서 사용할 수 있나요?
더 적절한 질문 표현
- "Redis Transaction 내에서 GET 결과를 다음 명령어에 사용할 수 있나요?"
- "Redis Transaction에서 read-modify-write 패턴을 구현할 수 있나요?"
왜 불가능한가?
MULTI 명령어 이후 발행되는 모든 명령어는 즉시 실행되지 않습니다. Redis 서버는 각 명령어에 대해 "QUEUED" 문자열만 응답하고, 실제 실행은 EXEC가 호출될 때 일괄적으로 수행됩니다.
Transaction tx = jedis.multi();
// 이 명령어들은 실행되지 않고 큐에만 저장됨
Response<String> tokensResponse = tx.hget(key, "tokens");
// 여기서 tokensResponse.get()을 호출하면 예외 발생!
// IllegalStateException: Response cannot be used before EXEC이 동작 방식 때문에 다음과 같은 read-modify-write 패턴을 Transaction만으로 구현할 수 없습니다:
- 값을 읽는다
- 애플리케이션에서 계산한다
- 결과를 저장한다
해결 방법 1: WATCH + Transaction
WATCH 명령어를 사용하면 낙관적 잠금(optimistic locking)을 구현할 수 있습니다. Redis 공식 문서 (opens in a new tab)
Redis 공식 문서에서 "WATCHed keys are monitored in order to detect changes against them. If at least one watched key is modified before the EXEC command, the whole transaction aborts" 라고 명시되어 있음.
// 1. WATCH로 키 모니터링 시작
jedis.watch(key);
// 2. Transaction 외부에서 값 읽기
String currentValue = jedis.get(key);
// 3. 애플리케이션에서 계산
int newValue = Integer.parseInt(currentValue) + 1;
// 4. Transaction 시작 및 업데이트
Transaction tx = jedis.multi();
tx.set(key, String.valueOf(newValue));
// 5. EXEC - 다른 클라이언트가 키를 변경했다면 null 반환
if (tx.exec() == null) {
// 재시도 필요
}주의: WATCH ~ MULTI 구간은 원자적이지 않으므로 Race Condition 발생 시 재시도가 필요합니다.
해결 방법 2: Lua Script (권장)
Lua Script는 Redis 서버에서 원자적으로 실행되므로 중간 값을 읽고 계산한 후 저장하는 작업이 완전히 원자적입니다. Rafael Eyng - Redis Pipelining, Transactions and Lua Scripts (opens in a new tab)
위 블로그에서 "In a Lua Script, we can use the return value of Redis commands to store values in Lua variables and use the values later" 라고 명시되어 있음.
String luaScript =
"local current = redis.call('GET', KEYS[1])\n" +
"local newValue = tonumber(current) + 1\n" +
"redis.call('SET', KEYS[1], newValue)\n" +
"return newValue";
Object result = jedis.eval(luaScript, 1, key);질문 2: Transaction과 Pipeline의 차이점
핵심 차이: 원자성 보장 여부
| 구분 | Pipeline | Transaction |
|---|---|---|
| 목적 | 네트워크 최적화 | 원자성 보장 |
| 원자성 | ❌ 없음 | ✅ 있음 |
| 다른 클라이언트 끼어들기 | ⚠️ 가능 | ❌ 불가능 |
| 중간 값 사용 | ❌ 불가능 | ❌ 불가능 |
Pipeline
Pipeline은 네트워크 왕복(round-trip)을 줄여 성능을 최적화합니다. 여러 명령어를 한 번의 통신으로 서버에 전송하지만, 실행 도중 다른 클라이언트의 명령어가 끼어들 수 있습니다. Redis 공식 문서 (opens in a new tab)
Redis 공식 문서에서 "Pipelines avoid network and processing overhead by sending several commands to the server together in a single communication" 라고 명시되어 있음.
Pipeline pipeline = jedis.pipelined();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
pipeline.sync();사용 시점:
- 원자성이 필요 없는 대량의 독립적인 명령어 실행
- 캐시 예열(warm-up)
- 대량 데이터 로딩
Transaction
Transaction은 MULTI/EXEC 블록 내의 모든 명령어가 순차적으로 중단 없이 실행됨을 보장합니다. Redis 공식 문서 (opens in a new tab)
Transaction tx = jedis.multi();
tx.decrBy("account:A", 100);
tx.incrBy("account:B", 100);
tx.exec();사용 시점:
- 여러 명령어가 원자적으로 실행되어야 할 때
- 계좌 이체 같은 금융 트랜잭션
- 재고 차감 같은 일관성이 중요한 작업
함께 사용하기
Pipeline과 Transaction을 함께 사용하면 네트워크 최적화와 원자성 보장을 동시에 얻을 수 있습니다. Stack Overflow - pipelining vs transaction in redis (opens in a new tab)
// Jedis에서는 Transaction이 자동으로 pipelining됨
Transaction tx = jedis.multi();
tx.set("key1", "value1");
tx.set("key2", "value2");
tx.exec();방법별 원자성 비교
| 방식 | 중간 값 사용 | 완전한 원자성 | 재시도 필요 | 권장 |
|---|---|---|---|---|
| Pipeline | ❌ 불가능 | ❌ 없음 | N/A | 성능 최적화용 |
| Transaction | ❌ 불가능 | ⚠️ 부분적 | N/A | 단순 원자성 |
| WATCH + Transaction | ⚠️ Transaction 외부에서만 | ⚠️ 조건부 | ✅ 필요 | 낙관적 잠금 |
| Lua Script | ✅ 가능 | ✅ 완전 보장 | ❌ 불필요 | read-modify-write |
실무 권장 사항
단순 원자적 실행이 필요한 경우: Transaction
Transaction tx = jedis.multi();
tx.decrBy("stock:item1", 1);
tx.lpush("orders:item1", orderId);
tx.exec();대량 명령어 성능 최적화: Pipeline
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("key:" + i, "value:" + i);
}
pipeline.sync();read-modify-write 패턴: Lua Script
String script =
"local stock = tonumber(redis.call('GET', KEYS[1]))\n" +
"if stock >= tonumber(ARGV[1]) then\n" +
" redis.call('DECRBY', KEYS[1], ARGV[1])\n" +
" return 1\n" +
"end\n" +
"return 0";
Long result = (Long) jedis.eval(script, 1, "stock:item1", "5");