[Redis] 동시성 제어
Redis는 캐시 저장소로 사용되지만.. 일단 "데이터베이스" 이니 일관성, 동시성, 신뢰성을 보장하기 위해 트랜잭션을 지원한다.
await isolatedClient.watch(itemsKey(attrs.itemId));
return isolatedClient
.multi()
.rPush(bidHistoryKey(attrs.itemId), serialized)
.hSet(itemsKey(item.id), {
bids:item.bids + 1,
price: attrs.amount,
highestBidUserId: attrs.userId
})
.zAdd(itemsByPriceKey(), {
value: item.id,
score: attrs.amount
})
.exec()
Redis는 MULTI 명령어로 트랜잭션을 시작한다.
이후 입력되는 명령어들은 실행 큐에 저장되고 EXEC 명령어가 실행될 때 큐에 있는 모든 명령어가 순차적으로 실행된다.
트랜잭션으로 묶은 명령어 중 하나라도 실패한다면 트랜잭션은 종료된다.
WATCH 명령어는 트랜잭션에서 특정 키를 모니터링해 트랜잭션 실행 전 키가 변경되는 경우 트랜잭션이 실패하도록 하는 Optimistic Lock 매커니즘을 제공한다.
예시에서는 node-redis 라이브러리로 itemsKey(attrs.itemId) 를 모니터링한다.
WATCH 이후 다른 클라이언트에 의해 해당 키 값이 변경된다면 EXEC 명령어는 실패하고 NULL 을 반환한다.
const client = createClient({
socket: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT)
},
password: process.env.REDIS_PW,
scripts: {
unlock: defineScript({
NUMBER_OF_KEYS: 1,
transformArguments(key: string, token: string) {
return [key, token]
},
transformReply(reply: any) {
return reply;
},
SCRIPT: `
if redis.call('GET', KEY[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
`
}),
addOneAndStore: defineScript({
NUMBER_OF_KEYS: 1,
SCRIPT : `
local keyToAssignIncrementedNumberTo = KEYS[1]
return redis.call('SET', KEYS[1], 1 + tonumber(ARGV[1]))
`,
transformArguments(key: string, value: number) {
return [key, value.toString()]
// evalsha id 1 books:count 5
},
transformReply(reply: any) {
return reply
}
}),
incrementView: defineScript({
NUMBER_OF_KEYS: 3,
SCRIPT: `
local itemsViewsKey = KEYS[1]
local itemsKey = KEYS[2]
local itemsByViewsKey = KEYS[3]
local itemId = ARGV[1]
local userId = ARGV[2]
local inserted = redis.call('PFADD', itemsViewsKey, userId)
if inserted == 1 then
redis.call('HINCRRBY', itemsKey, 'views', 1)
redis.call('ZINCRBY', itemsByViewsKey, 1, itemId)
end
`,
transformArguments(itemId: string, userId: string) {
return [
itemsViewsKey(itemId),
itemsKey(itemId),
itemsByViewsKey(),
itemId,
userId
];
},
transformReply() {}
})
}
});
node-redis 라이브러리를 사용해서 기본적인 Redis 명령어를 실행할 수 있지만, 몇 가지 이유로 Redis와 통신할 때 Lua Script를 사용하기도 한다.
Lua Script를 Redis 서버에 업로드하고 실행할 때는 EVAL 이나 EVALSHA 명령어를 사용한다.
redis.call('set', 'key', 'value')
return redis.call('get', 'key')
EVAL "redis.call('set', 'key', 'value'); return redis.call('get', 'key')" 0
EVALSHA 명령어는 Lua Script의 SHA1 해시를 사용해서 스크립트를 실행해 한 번 로드된 스크립트를 여러 번 사용할 때 효과적이다.
예시는 node-redis 라이브러리에서 defineScript를 사용해 Lua Script를 정의하고 실행한다.
내부적으로는 Redis의 EVALSHA 명령어를 사용해 스크립트를 실행하고, transformArguments와 transformReply로 스크립트 실행에 필요한 인자와 결과를 변환한다.
node-redis로 Redis 서버에 명령어를 실행하는 경우 기본적으로 각 명령어가 개별적으로 실행되지만, Lua Script로 Redis 서버와 통신하는 경우 스크립트를 모두 원자적으로 실행해 동시성 문제를 해결할 수 있다.
이 외에도 Lua Script는 Redis 서버에서 바로 실행되니.. 네트워크 통신 오버헤드를 줄일 수 있고, 복잡한 로직이나 조건부 처리를 script로 처리할 수 있다.
굳이 비교하자면.. node-redis는 JPA를 사용해 추상화 정도가 좀 더 높고, Lua Script는 Native SQL을 사용해 추상화 정도가 좀 더 낮다는 맥락으로 이해하면 된다.
일반적인 RDBMS에서는 Lock 메커니즘을 제공해 데이터 일관성과 트랜잭션을 관리하는데, Redis에서는 따로 Lock 메커니즘을 제공하지 않아 필요 시 사용자가 직접 패턴을 설계해야 한다.
'Database > Redis' 카테고리의 다른 글
[Redis] Stream (0) | 2024.10.10 |
---|---|
[Redis] Module - RediSearch (0) | 2024.10.06 |
[Redis] 파이프라인과 자료구조 (0) | 2024.05.15 |
[Redis] 캐시 서버와 명령어 (0) | 2024.04.27 |
댓글
이 글 공유하기
다른 글
-
[Redis] Stream
[Redis] Stream
2024.10.10 -
[Redis] Module - RediSearch
[Redis] Module - RediSearch
2024.10.06 -
[Redis] 파이프라인과 자료구조
[Redis] 파이프라인과 자료구조
2024.05.15 -
[Redis] 캐시 서버와 명령어
[Redis] 캐시 서버와 명령어
2024.04.27