이전 글(DB에서 해결)의 시리즈 입니다. 따라서 예시의 자세한 상황은 이전 글을 참고해주세요.
간단한 상황
주문 번호의 스펙을 다시 설명하면 다음과 같습니다.
- 주문 번호는 매일 초기화
- 주문 번호는 가게별로 다름
- 같은 날 같은 가게에는 동일한 주문 번호가 없음
- 주문 번호는 1부터 시작해서 1씩 증가
레디스 도입 이유
MySQL
에서 Redis
로 변경하려는 이유는 다음과 같습니다.
- 빠른 속도
DB
에 부하를 줄이기 위함- 이미 레디스를 사용 중
레디스의 동시성 문제
레디스는 싱글 스레드로 동작하기 때문에 동시성 문제
가 발생하지 않습니다. 하지만 이것은 단일 레디스만 사용하는 경우입니다. 보통 레디스를 멀티 스레드 환경에서 사용하기 때문에 동시성 문제
가 발생할 수 있습니다.
INCR
을 사용하면 현재 상황에서는 동시성이 발생하지 않습니다. 하지만 동시성 해결이 주제라 일부로 동시성 문제를 발생 시키겠습니다.
동시성이 발생하는 상황
주문번호를 가져오고 증가시키는 코드는 다음과 같습니다.
1 | 127.0.0.1:6379> get store1 |
store1의 값을 가져오고 증가시키는 코드입니다. 현재의 주문 번호는 1입니다.
문제 발생
위의 그림을 보면 get store1
을 한 후에 컨텍스트 스위칭이 발생하면, get store1
의 값이 동일한 값이 나올 수 있습니다.
Transaction사용
Transaction
을 사용하면 원자성
을 보장할 수 있습니다. MULTI
를 통해서 트랜잭션을 시작하고 EXEC
/DISCARD
를 통해서 트랜잭션을 실행/취소합니다.
원리는 MULTI
로 트랜잭션을 시작하면 queue
에 명령어를 넣고 EXEC
를 하면 queue
에 존재하는 명령어가 순차적으로 실행됩니다. 그래서 트랜잭션이 실행되는 동안 다른 명령어가 실행되지 않습니다.
특이한 점은 트랜잭션
이 실행되는 동안 중간 결과
를 확인할 수 없습니다.
1 | 127.0.0.1:6379> MULTI |
EXEC를 하기 전에는 get store1
의 결과를 알 수 없습니다. 따라서 get store1
의 결과에 +1의 값을 set store1 [value]
에 넣을 수 없습니다.
문제 상황
그림처럼 트랜잭션을 사용하면 트랜잭션 동안 race condition
이 발생하지 않지만, 트랜잭션이 끝나고 get store1
을 했을 때 컨텍스트 스위칭이 발생하면 race condition
이 발생할 수 있습니다.
Pipeline
redis pipeline의 기본적인 아이디어는 한번에 여러 명령어를 보내는 것입니다. 클라이언트가 명령어에 대한 응답을 받기 전에 다음 명령어를 보내기 때문에 round-trip time(RTT)
가 적습니다. 따라서 여러 명령어를 보낼 때 pipeline
을 사용하면 성능이 향상됩니다.
한번에 여러 명령어를 보내서 원자성
을 보장하는 것 처럼 보이지만 pipeline
은 원자성
을 보장하지 않습니다. 왜냐하면 다른 pipeline
이 끼어들어서 명령어를 실행할 수 있기 때문입니다.
redis Lua Script
Lua Script를 사용해서 문제를 해결할 수 있습니다. Lua Script
는 Redis
에서 제공하는 스크립트 언어입니다. Lua Script
를 사용하면 스크립트가 실행되는 동안 다른 명령어가 실행되지 않기 때문에 race condition
이 발생하지 않습니다.
예제
lua script
1
2
3
4
5
6
7
8
9
10local key = KEYS[1]
local currentValue = redis.call('GET', key)
if currentValue == false then
currentValue = 0
else
currentValue = tonumber(currentValue)
end
local newValue = currentValue + 1
redis.call('SET', key, tostring(newValue))
return newValueredis
1
2127.0.0.1:6379> EVAL "local key = KEYS[1] local currentValue = redis.call('GET', key) if currentValue == false then currentValue = 0 else currentValue = tonumber(currentValue) end local newValue = currentValue + 1 redis.call('SET', key, tostring(newValue)) return newValue" 1 store1
(integer) 1
이렇게 하면 race condition
을 방지할 수 있고, 중간 결과
를 확인할 수 있습니다.
Lua Script 주의사항
앞서 말했듯이 Lua script
가 실행 된다면 Lua script
가 실행되는 동안 다른 명령어가 실행되지 않습니다. 그래서 오래 걸리는 작업을 할 때는 Lua script
를 사용하지 않는 것이 좋습니다.
pipeline vs Lua Script
사실 Redis 2.6
버전 이상에서는 여러 명령어를 보낼 때도 대부분의 상황에서 Lua Script
가 pipeline
보다 낫습니다.
공식 문서를 보면 응답값을 활용하는 경우 pipeline
은 읽기 명령어를 클라이언트
가 받은 다음에 응답을 하지만, Lua Script
는 Redis 서버
에서 실행되기 때문에 pipeline
보다 빠릅니다.
결론(특징)
Transaction
- 원자성이 필요할 때
- 다음 명령어가 이전 명령어의 응답이 필요 없을 때
Pipeline
- Redis 서버로 여러 명령어를 보낼 때
- 다음 명령어가 이전 명령어의 응답이 필요 없을 때
Lua Script
- 원자성이 필요할 때
- 명령어를 실행하는 동안 중간 값이 필요할 때
- 조건부 명령어를 실행할 때
- 단 오래 걸리는 작업은 주의