본 내용은 여러 스레드에서 같은 키를 read하고 계산하여 비동기적으로 write을 할 때 생기는 정합성 문제를 해결하기 위해 작성했습니다.
일반적인 DB의 프로시져와 같이 Redis에도 스크립트를 하나의 명령어로 처리하는 방법이 존재합니다. read, 계산 처리, write하는 동작을 하나의 lua script로 묶어서 처리하면 해당 동작이 atomic 하다고 할 수 있어 데이터의 정합성을 보장할 수 있습니다.
우선 여러 동작들을 테스트 해보기 위해 redis-cli 를 통해 레디스에 접속했다고 가정합니다.
EVAL 명령어는 뒤의 내용을 lua script로 Redis에서 실행한다는 의미입니다.
> EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"
Redis에서 절대! 추천하진 않지만 동적으로 소스코드를 동적으로 실행시킬수도 있습니다. 일반적으로 동적으로 소스코드를 실행시키는 짓은 하지 않는게 좋습니다. 공식 문서에서는 Redis에 lua script를 Cache하여 사용하는 것을 권장합니다.
> EVAL "return 'Hello'" 0
"Hello"
> EVAL "return 'Scripting!'" 0
"Scripting!"
아래에서는 인자로 받을 값들을 어떻게 작성하는지 알 수 있습니다.
스크립트에서 인자로 받는 값은 크게 키 개수, 키 배열, 인자 배열입니다.
스크립트에서 어떻게 그냥 문자열의 나열을 구별할까요?
키 배열과 인자 배열 앞에 키 개수를 명시해서 구별합니다.
이 때, lua script의 배열의 element index는 1부터 시작하는 것이 다른 프로그램 언어와의 두드러진 차이점입니다.
While key names in Redis are just strings, unlike any other string values, these represent keys in the database. The name of a key is a fundamental concept in Redis and is the basis for operating the Redis Cluster.
Redis의 키 이름은 단순한 문자열이지만 다른 문자열 값과 달리 데이터베이스의 키를 나타냅니다. 키 이름은 Redis의 기본 개념이자 Redis Cluster를 운영하는 기반이 됩니다.
EVAL "__" [키 개수] [키 배열] [인자 배열]
> EVAL "return ARGV[1]" 0 Hello # 키 개수 0개 뒤의 내용은 모두 인자 배열
"Hello" # ARGV[1]은 즉 인자배열의 1번째 원소값
Important: to ensure the correct execution of scripts, both in standalone and clustered deployments, all names of keys that a script accesses must be explicitly provided as input key arguments.
싱글이나, 클러스터 상황에서 스크립트가 올바르게 실행되는 걸 보장하려면, 스크립트가 엑세스 하는 모든 키는 키 인자값에 반드시 명시적으로 인풋을 받아야 한다.
키 배열에 들어가 있지 않은 모든 인자들은 ARGV 배열의 인자가 됩니다.
> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"
예시 )
> flushall
OK
> keys *
(empty array)
> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK
> keys *
1) "foo"
> EVAL "return redis.call('SET', KEYS[2], ARGV[2])" 2 foo1 foo2 bar1 bar2
OK
> keys *
1) "foo"
2) "foo2"
SCRIPT LOAD 명령어를 통해 스크립트를 캐시할 수 있습니다.
> SCRIPT LOAD "return 'Hello lua Script'"
"69b5e3d6d95429afe376905c9fc0c064802febe7"
> EVALSHA "69b5e3d6d95429afe376905c9fc0c064802febe7" 0
"Hello lua Script"
단 영속적이진 않기 때문에 레디스 서버를 재시작하거나, fail-over시 slave가 master로 승격하거나, SCRIPT FLUSH를 하는 경우 캐시된 스크립트는 언제든 사라질 수 있습니다.
이부분은 대부분의 Redis client들이 위의 경우가 발생했을 때 자동으로 캐시하게 하는 API를 작성되어 있다고 하니 사용하는 Redis Client 문서를 잘 찾아봅시다.
파이프라인화된 요청의 context에서 EVALSHA를 실행할 때는 특별히 주의해야 합니다.
한 클라이언트의 파이프라인 요청 명령들은 보낸 순서대로 실행되지만, 다른 클라이언트의 명령이 중간에 끼어들 수 있습니다. 이 때문에 NOSCRIPT 에러가 발생할 수 있지만 핸들링이 불가능합니다.
lua script에서 키를 여러 개 사용하는 스크립트를 실행하고자 한다면 키를 만들 때 해시 태그를 이용해서 임의의 슬롯에 키를 몰아넣어야 합니다. 여러 슬롯에 키가 분배되어 있다면 lua script에서 정상적으로 동작하지 않습니다.
해시 태그
키의 일부를 중괄호({})로 감싸면 해당 부분이 일종의 태그처럼 인식되며 같은 태그를 갖는 키는 반드시 같은 슬롯에 포함됩니다.
예를들어 DEVHANS가 해쉬되어 1388 슬롯에 분배된다면 {DEVHANS}:sum,{DEVHANS}:cnt,{DEVHANS}:max,{DEVHANS}:min은 모두 중괄호 안의 DEVHANS만 해쉬하여 모두 1388 슬롯으로 분배될 것입니다.
get {DEVHANS}:sum
get {DEVHANS}:cnt
get {DEVHANS}:max
get {DEVHANS}:min
[REF]
레디스 공식 문서 : https://redis.io/docs/interact/programmability/eval-intro/
명시적으로 input을 받아야 하는 이유 : https://stackoverflow.com/questions/38234507/why-cant-my-redis-lua-script-atomically-update-keys-on-different-redis-cluster
라인 엔지니어 블로그 : https://engineering.linecorp.com/ko/blog/atomic-cache-stampede-redis-lua-script