Lua로 스크립팅하기
Lua로 스크립팅하기 (Scripting with Lua)
Redis에서는 사용자가 Lua 스크립트를 서버에서 업로드하고 실행할 수 있어요. 스크립트는 프로그래밍 제어 구조를 쓸 수 있고, 실행 중에 대부분의 명령을 사용해 데이터베이스에 접근할 수 있어요. 스크립트가 서버 안에서 실행되므로 데이터를 읽고 쓰는 효율이 매우 높죠.
Redis는 스크립트의 원자적(atomic) 실행을 보장해요. 스크립트가 실행되는 동안, 그 전체 실행 시간 동안 모든 서버 활동이 차단돼요. 이 의미는 스크립트의 모든 영향이 "아직 일어나지 않았거나" "이미 일어났다"는 뜻이에요. 즉 중간 상태가 없다는 거죠.
스크립팅은 여러 경우에 유용한 속성들을 제공해요.
- 데이터가 있는 곳에서 로직을 실행하는 locality를 제공해요. 데이터 지역성은 전체 레이턴시를 줄이고 네트워크 리소스를 아껴요.
- 스크립트의 원자적 실행을 보장하는 차단(blocking) 의미론을 제공해요.
- Redis에 없거나 있기엔 너무 niche한 단순한 기능의 조합을 가능하게 해요.
Lua를 쓰면 애플리케이션 로직의 일부를 Redis 안에서 돌릴 수 있어요. 이런 스크립트는 여러 키에 걸친 조건부 업데이트를 수행하고, 여러 데이터 타입을 원자적으로 결합할 수도 있어요.
스크립트는 내장 실행 엔진에 의해 Redis에서 실행돼요. 현재 Redis는 하나의 스크립팅 엔진, Lua 5.1 인터프리터를 지원해요. 완전한 문서는 Redis Lua API Reference 페이지를 참고하세요.
서버가 실행하지만, Eval 스크립트는 클라이언트측 애플리케이션의 일부로 간주돼요. 그래서 이름이 붙거나 버전 관리되거나 영속화되지 않아요. 즉 서버 재시작, replica로의 failover 등으로 누락되면 애플리케이션이 언제든 다시 로드해야 할 수 있어요. 버전 7.0부터는 Redis Functions가 프로그래밍 가능성에 대한 대안적 접근을 제공하는데, 서버 자체를 추가적인 프로그램 로직으로 확장할 수 있게 해줘요.
출처: 공식문서
시작하기
EVAL 명령으로 스크립팅을 시작해 볼게요.
> EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"
여기서 EVAL은 두 개의 인자를 받아요. 첫 번째는 스크립트의 Lua 소스 코드로 된 문자열이에요. 스크립트는 Lua 함수 정의를 포함할 필요가 없어요. 그냥 Redis 엔진의 컨텍스트에서 실행될 Lua 프로그램일 뿐이죠.
두 번째 인자는 스크립트 본문 뒤에 오는 인자의 숫자로, 세 번째 인자부터 Redis 키 이름을 나타내요. 이 예시에서는 스크립트에 아무 인자(키 이름이든 아니든)를 제공하지 않았으므로 0을 썼어요.
스크립트 매개변수화 (Script parameterization)
애플리케이션이 필요에 따라 스크립트 소스 코드를 동적으로 생성하는 것도 가능은 하지만, 매우 권장되지 않아요. 예를 들어 애플리케이션이 이렇게 완전히 다르면서도 동시에 완벽히 동일한 두 스크립트를 보낼 수 있어요:
redis> EVAL "return 'Hello'" 0
"Hello"
redis> EVAL "return 'Scripting!'" 0
"Scripting!"
이런 운영 방식은 Redis가 차단하지 않지만, 스크립트 캐시 고려 사항 때문에 안티 패턴이에요(자세한 내용은 아래에). 애플리케이션이 같은 스크립트의 미묘한 변형들을 생성하는 대신, 스크립트를 매개변수화하고 실행에 필요한 인자를 넘기는 게 좋아요.
다음 예시는 위와 같은 효과를 매개변수화로 얻는 방법이에요:
redis> EVAL "return ARGV[1]" 0 Hello
"Hello"
redis> EVAL "return ARGV[1]" 0 Parameterization!
"Parameterization!"
여기서 Redis가 키 이름인 입력 인자와 그렇지 않은 입력 인자를 구분한다는 점을 이해하는 게 중요해요.
Redis에서 키 이름은 그냥 문자열이지만, 다른 문자열 값들과 달리 데이터베이스의 키를 나타내요. 키의 이름은 Redis의 근본적인 개념이고 Redis Cluster 운영의 기반이 돼요.
중요: 단일(standalone) 배포와 클러스터 배포 모두에서 스크립트가 올바르게 실행되도록, 스크립트가 접근하는 모든 키 이름은 반드시 입력 키 인자로 명시적으로 제공돼야 해요. 스크립트는 입력 인자로 주어진 이름의 키에만 접근해야 하죠. 프로그램적으로 생성한 이름이나 DB에 저장된 데이터 구조의 내용에 기반한 키에는 절대 접근하면 안 돼요.
키 이름이 아닌 함수 입력은 일반 입력 인자(regular input argument) 예요.
위 예시에서 Hello와 *Parameterization!*은 모두 일반 입력 인자예요. 스크립트가 어떤 키도 건드리지 않으므로, 키 이름 인자가 없다는 뜻으로 숫자 0을 썼어요. 실행 컨텍스트는 KEYS와 ARGV 런타임 전역 변수를 통해 인자를 스크립트에 노출해요. KEYS 테이블은 실행 전에 스크립트에 제공된 모든 키 이름 인자로 미리 채워지고, ARGV 테이블은 일반 인자에 대해 비슷한 역할을 해요.
입력 인자가 KEYS와 ARGV 런타임 전역 변수에 어떻게 분배되는지 보여주는 예시예요:
redis> 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"
참고: 위에서 보듯 Lua의 테이블 배열은 RESP2 array replies로 반환되므로, 클라이언트 라이브러리가 이를 프로그래밍 언어의 네이티브 배열 타입으로 변환할 가능성이 커요. 데이터 타입 변환 규칙을 참고하세요.
스크립트에서 Redis와 상호작용하기
Lua 스크립트에서 redis.call()이나 redis.pcall()로 Redis 명령을 호출할 수 있어요.
둘은 거의 동일해요. 둘 다 잘 구성된 명령이라면 Redis 명령을 주어진 인자와 함께 실행해요. 차이는 런타임 오류(예: 문법 오류)를 처리하는 방식에 있어요. redis.call() 호출에서 발생한 오류는 이를 실행한 클라이언트에 직접 반환되고, 반대로 redis.pcall() 호출에서 만난 오류는 가능한 처리를 위해 스크립트의 실행 컨텍스트로 반환돼요.
예를 들어:
> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK
이 스크립트는 키 이름 하나와 값 하나를 입력 인자로 받아요. 실행되면 SET 명령으로 키 foo에 문자열 값 "bar"를 설정해요.
스크립트 캐시 (Script cache)
지금까지는 EVAL 명령으로 스크립트를 실행했어요. EVAL을 호출할 때마다 요청에 스크립트 소스 코드도 함께 포함돼요. 같은 매개변수화 스크립트 집합을 실행하기 위해 EVAL을 반복 호출하는 것은 네트워크 대역폭을 낭비하고 Redis에 오버헤드도 줘요. 그래서 Redis는 스크립트용 캐싱 메커니즘을 제공해요.
EVAL로 실행하는 모든 스크립트는 서버가 유지하는 전용 캐시에 저장돼요. 캐시의 내용은 스크립트의 SHA1 다이제스트로 정리되므로, 스크립트의 SHA1 다이제스트는 캐시에서 그 스크립트를 고유하게 식별해요. EVAL을 실행하고 INFO를 호출하면 used_memory_scripts_eval과 number_of_cached_scripts 메트릭이 새 스크립트를 실행할 때마다 증가하는 걸 확인할 수 있어요.
앞서 말했듯 동적 생성 스크립트는 안티 패턴이에요. 애플리케이션 런타임에 스크립트를 생성하면 호스트의 캐싱용 메모리 리소스를 고갈시킬 수 있어요. 대신 스크립트는 최대한 일반적이고, 인자를 통해 맞춤 실행을 제공해야 해요.
SCRIPT LOAD 명령으로 소스 코드를 제공하면 스크립트가 서버 캐시에 로드돼요. 서버는 실행하지 않고 컴파일해 캐시에 로드하기만 해요. 로드된 후에는 서버가 반환한 SHA1 다이제스트로 캐시된 스크립트를 실행할 수 있어요.
로드 후 캐시 스크립트를 실행하는 예시예요:
redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script"
캐시 휘발성 (Cache volatility)
Redis 스크립트 캐시는 항상 휘발적(volatile) 이에요. 데이터베이스의 일부로 간주되지 않고 영속화되지 않아요. 캐시는 서버 재시작, replica가 master 역할을 맡는 failover, 혹은 SCRIPT FLUSH로 명시적으로 비워질 수 있어요. 즉 캐시된 스크립트는 일시적이고, 캐시 내용은 언제든 사라질 수 있어요.
스크립트를 쓰는 애플리케이션은 항상 EVALSHA로 실행해야 해요. 스크립트의 SHA1 다이제스트가 캐시에 없으면 서버가 오류를 반환해요:
redis> EVALSHA ffffffffffffffffffffffffffffffffffffffff 0
(error) NOSCRIPT No matching script
이 경우 애플리케이션은 먼저 SCRIPT LOAD로 로드하고, 다시 EVALSHA를 호출해 SHA1로 실행해야 해요. 대부분의 Redis 클라이언트는 이 작업을 자동으로 해주는 유틸리티 API를 제공해요.
파이프라이닝 맥락에서의 EVALSHA
pipelined request 맥락에서 EVALSHA를 실행할 때는 특별히 주의해야 해요. 파이프라인 요청의 명령들은 보낸 순서대로 실행되지만, 그 사이에 다른 클라이언트의 명령이 끼어들 수 있어요. 그래서 NOSCRIPT 오류가 파이프라인 요청에서 반환될 수 있는데, 이를 처리할 방법이 없어요.
따라서 클라이언트 라이브러리 구현은 파이프라인 맥락에서 매개변수화된 일반 EVAL로 되돌아가야 해요.
스크립트 캐시 의미론 (Script cache semantics)
정상 운영 중에는 애플리케이션의 스크립트가 캐시에 무기한 남아 있게(즉 서버가 재시작되거나 캐시가 비워질 때까지) 설계됐어요. 잘 작성된 애플리케이션의 스크립트 캐시 내용은 계속 늘어나지 않을 가능성이 크다는 게 근거예요. 수백 개의 캐시 스크립트를 쓰는 큰 애플리케이션조차 캐시 메모리 사용 측면에선 문제가 되지 않아요.
스크립트 캐시를 비우는 유일한 방법은 SCRIPT FLUSH 명령을 명시적으로 호출하는 거예요. 이 명령은 캐시를 완전히 비워 지금까지 실행한 모든 스크립트를 제거해요. 보통 클라우드 환경에서 인스턴스를 다른 고객·애플리케이션에 재할당할 때만 필요해요.
또 이미 말했듯, Redis 인스턴스 재시작은 비영속 스크립트 캐시를 비워요. 그래도 클라이언트 관점에서 두 명령 사이에 Redis 인스턴스가 재시작되지 않았다고 확신하는 방법은 두 가지뿐이에요.
- 서버와의 연결이 영속적이고 지금까지 한 번도 닫히지 않았을 때.
INFO명령의run_id필드를 클라이언트가 명시적으로 확인해 서버가 재시작되지 않고 같은 프로세스임을 확인할 때.
실용적으로는, 클라이언트가 "주어진 연결 맥락에서 관리자가 SCRIPT FLUSH를 명시적으로 호출하지 않는 한 캐시 스크립트는 보장된다"고 가정하는 게 훨씬 단순해요. Redis가 캐시 스크립트를 보존한다고 신뢰할 수 있다는 사실은 파이프라이닝 맥락에서 의미론적으로 도움이 돼요.
SCRIPT 명령
Redis SCRIPT 명령은 스크립팅 하위 시스템을 제어하는 여러 방법을 제공해요:
SCRIPT FLUSH: 스크립트 캐시를 강제로 비우는 유일한 명령이에요. 같은 Redis 인스턴스를 다른 용도에 재할당하는 환경에서 가장 유용하고, 클라이언트 라이브러리 구현 테스트에도 도움이 돼요.SCRIPT EXISTS: 하나 이상의 SHA1 다이제스트를 인자로 받아 1과 0의 배열을 반환해요. 1은 해당 SHA1이 스크립팅 캐시에 이미 있는 스크립트로 인식됐다는 뜻이고, 0은 이 SHA1을 가진 스크립트가 (적어도 최근SCRIPT FLUSH이후로는) 로드된 적 없다는 뜻이에요.SCRIPT LOAD script: 지정된 스크립트를 Redis 스크립트 캐시에 등록해요.EVALSHA가 실패하지 않도록 보장하고 싶은 모든 맥락(예: 파이프라인,MULTI/EXEC트랜잭션에서)에서 유용해요. 스크립트를 실행할 필요는 없어요.SCRIPT KILL: 서버를 종료하지 않고 오래 실행되는 스크립트(느린 스크립트)를 중단하는 유일한 명령이에요. 실행 시간이 설정된 최대 실행 시간 임계값을 넘으면 그 스크립트를 느리다고 판단해요.SCRIPT KILL은 실행 중 데이터셋을 수정하지 않은 스크립트에만 쓸 수 있어요(읽기 전용 스크립트를 멈춰도 원자성은 깨지지 않으므로).SCRIPT DEBUG: 내장 Redis Lua 스크립트 디버거 사용을 제어해요.
스크립트 복제 (Script replication)
단일 배포에서 master라 불리는 하나의 Redis 인스턴스가 전체 데이터베이스를 관리해요. 클러스터 배포는 샤딩된 데이터베이스를 관리하는 master가 최소 3개 있어요. Redis는 복제로 주어진 마스터에 대한 하나 이상의 replica(정확한 복사본)를 유지해요.
스크립트가 데이터를 수정할 수 있으므로 Redis는 일관성 유지를 위해 스크립트가 수행한 모든 쓰기 연산도 replica로 보내는 걸 보장해요. 스크립트 복제에는 두 가지 개념적 접근이 있어요:
- Verbatim(원문) 복제: master가 스크립트 소스 코드를 replica에 보내요. replica는 스크립트를 실행해 쓰기 효과를 적용해요. 짧은 스크립트가 많은 명령을 생성하는 경우(예: for 루프) 복제 대역폭을 아낄 수 있어요. 하지만 replica가 master가 한 것과 같은 작업을 다시 하므로 낭비적이에요. 더 중요하게, 모든 쓰기 스크립트가 결정적(deterministic)이어야 해요.
- Effects 복제: 스크립트의 데이터 수정 명령만 복제돼요. replica는 스크립트를 실행하지 않고 명령을 실행해요. 네트워크 트래픽은 더 길어질 수 있지만, 이 복제 방식은 정의상 결정적이라 특별한 고려가 필요 없어요.
Verbatim 스크립트 복제는 Redis 3.2까지 유일한 방식이었고, 3.2에서 effects 복제가 추가됐어요. lua-replicate-commands 설정 지시어와 redis.replicate_commands() Lua API로 활성화할 수 있어요.
Redis 5.0에서 effects 복제가 기본 모드가 됐고, Redis 7.0부터 verbatim 복제는 더 이상 지원되지 않아요.
스크립트 대신 명령 복제 (Replicating commands instead of scripts)
Redis 3.2부터 대체 복제 방법을 선택할 수 있어요. 전체 스크립트를 복제하는 대신, 스크립트가 생성한 쓰기 명령을 복제할 수 있어요. 이를 script effects replication이라 불러요.
참고: Redis 5.0부터 script effects replication이 기본 모드라 명시적으로 활성화할 필요가 없어요.
이 복제 모드에서 Lua 스크립트가 실행되는 동안, Redis는 Lua 스크립팅 엔진이 실행하는 데이터셋을 실제로 수정하는 모든 명령을 수집해요. 스크립트 실행이 끝나면, 스크립트가 생성한 명령 시퀀스가 MULTI/EXEC 트랜잭션으로 감싸져 replica와 AOF로 보내져요.
이것은 사용 사례에 따라 여러 면에서 유용해요:
- 스크립트 계산은 느린데 효과를 몇 개의 쓰기 명령으로 요약할 수 있다면, replica나 AOF 로드 시 다시 계산하는 건 아까워요. 그럴 때는 스크립트의 효과만 복제하는 게 훨씬 좋아요.
- 효과 복제가 활성화되면 비결정적 함수에 대한 제약이 사라져요. 예를 들어 스크립트 안 어디서든
TIME이나SRANDMEMBER명령을 자유롭게 쓸 수 있어요. - 이 모드에서 Lua PRNG는 매 호출마다 무작위로 시드돼요.
서버 설정이나 기본값으로 아직 활성화되지 않았다면(Redis 7.0 이전), 스크립트가 쓰기를 수행하기 전에 다음 Lua 명령을 실행해야 해요:
redis.replicate_commands()
redis.replicate_commands() 함수는 effects 복제가 활성화되면 _true)를 반환하고, 스크립트가 이미 쓰기 명령을 호출한 뒤였다면 false를 반환해 일반 전체 스크립트 복제를 사용해요.
이 함수는 Redis 7.0부터 폐기됐고, 아직 호출할 수는 있지만 항상 성공해요.
결정적 쓰기를 가진 스크립트 (Scripts with deterministic writes)
참고: Redis 5.0부터 스크립트 복제는 기본적으로 effects 기반이지 verbatim이 아니에요. Redis 7.0에서 verbatim 스크립트 복제는 완전히 제거됐어요. 다음 섹션은 Redis 7.0 미만 버전에서 effects 기반 스크립트 복제를 쓰지 않을 때만 해당돼요.
스크립팅의 중요한 부분은 결정적인 방식으로만 데이터베이스를 변경하는 스크립트를 작성하는 거예요. (버전 5.0까지 기본적으로) Redis 인스턴스에서 실행된 스크립트는 결과 명령이 아니라 스크립트 자체를 보내 replica와 AOF 파일로 전파돼요. 원격 호스트에서 스크립트가 다시 실행되므로(또는 AOF 재로드 시) 데이터베이스에 대한 변경이 재현 가능해야 해요.
스크립트를 보내는 이유는 스크립트가 생성하는 여러 명령을 보내는 것보다 훨씬 빠른 경우가 많기 때문이에요. 클라이언트가 master에 많은 스크립트를 보내면, 스크립트를 replica/AOF용 개별 명령으로 변환하는 것은 복제 링크나 Append Only File에 너무 많은 대역폭을 쓰게 돼요(네트워크로 받은 명령을 디스패치하는 게 Lua 스크립트가 호출한 명령을 디스패치하는 것보다 Redis에 훨씬 큰 CPU 부담이 되기 때문이기도 해요).
일반적으로 스크립트의 효과가 아니라 스크립트를 복제하는 게 합리적이지만, 모든 경우에 그렇진 않아요. 그래서 Redis 3.2부터 스크립팅 엔진은 스크립트 자체 대신 스크립트 실행으로 생긴 쓰기 명령 시퀀스를 복제할 수도 있어요.
이 섹션에서는 스크립트가 전체 스크립트를 보내는 방식으로 verbatim 복제된다고 가정해요. 이 복제 모드를 verbatim scripts replication(전체 스크립트 복제) 라 부를게요.
전체 스크립트 복제 방식의 주요 단점은 스크립트가 다음 속성을 가져야 한다는 거예요: 스크립트는 항상 같은 입력 데이터셋에 대해 같은 인자로 같은 Redis 쓰기 명령을 실행해야 해요. 스크립트가 수행하는 연산은 스크립트 실행이 진행되면서 또는 실행 사이에 바뀔 수 있는 숨은(비명시적) 정보나 상태에 의존할 수 없어요. 외부 I/O 장치의 입력에도 의존할 수 없어요.
시스템 시간을 쓰거나, 랜덤 값을 반환하는 Redis 명령(예: RANDOMKEY)을 호출하거나, Lua 난수 생성기를 쓰는 것 같은 행위는 일관되게 평가되지 않는 스크립트로 이어질 수 있어요.
스크립트의 결정적 동작을 강제하기 위해 Redis는 다음을 수행해요:
- Lua는 시스템 시간이나 외부 상태에 접근하는 명령을 내보내지 않아요.
- 스크립트가 Redis의 random 명령(
RANDOMKEY,SRANDMEMBER,TIME등) 이후에 데이터셋을 바꿀 수 있는 명령을 호출하면 오류로 스크립트를 차단해요. 즉 데이터셋을 수정하지 않는 읽기 전용 스크립트는 그런 명령을 호출할 수 있어요. random 명령이 반드시 난수를 쓰는 명령을 뜻하지는 않는다는 점에 주의하세요. 비결정적 명령은 모두 random 명령으로 간주돼요(가장 좋은 예가TIME). - Redis 4.0에서
SMEMBERS같은 임의 순서로 요소를 반환할 수 있는 명령(R sets는 unordered이므로)은 Lua에서 호출될 때 다른 동작을 보이며, Lua 스크립트에 데이터를 반환하기 전에 조용한 사전식(lexicographical) 정렬 필터를 거쳐요. 즉redis.call("SMEMBERS",KEYS[1])은 항상 같은 순서로 Set 요소를 반환하는 반면, 같은 명령을 일반 클라이언트가 호출하면 키에 정확히 같은 요소가 있어도 다른 결과를 줄 수 있어요. 단 Redis 5.0부터는 효과 복제가 이 비결정성을 우회하므로 이 정렬을 더 이상 수행하지 않아요. 일반적으로 Redis 4.0을 대상으로 개발할 때도 Lua에서 특정 명령이 정렬된다고 가정하지 말고, 호출하는 원래 명령의 문서에서 제공 속성을 확인하세요. - Lua의 의사 난수 생성 함수
math.random은 수정되어 항상 모든 실행에 같은 시드를 써요. 이는math.random을 호출하면 스크립트를 실행할 때마다 항상 같은 수열을 생성한다는 뜻이에요(math.randomseed를 쓰지 않는 한).
그래도 간단한 트릭으로 쓰기와 랜덤 동작을 함께 쓸 수 있어요. N개의 랜덤 정수로 리스트를 채우는 Redis 스크립트를 쓴다고 상상해 보세요. Ruby 초기 구현은 이렇게 생겼어요:
require 'rubygems'
require 'redis'
r = Redis.new
RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
while (i > 0) do
res = redis.call('LPUSH',KEYS[1],math.random())
i = i-1
end
return res
EOF
r.del(:mylist)
puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])
이 코드가 실행될 때마다 결과 리스트는 정확히 같은 요소를 가지게 돼요:
redis> LRANGE mylist 0 -1
1) "0.74509509873814"
2) "0.87390407681181"
3) "0.36876626981831"
4) "0.6921941534114"
5) "0.7857992587545"
6) "0.57730350670279"
7) "0.87046522734243"
8) "0.09637165539729"
9) "0.74990198051087"
10) "0.17082803611217"
스크립트를 결정적으로 만들면서도 여전히 다른 랜덤 요소를 생성하게 하려면, Lua 의사 난수 생성기의 시드인 추가 인자를 스크립트에 넣을 수 있어요:
RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
math.randomseed(tonumber(ARGV[2]))
while (i > 0) do
res = redis.call('LPUSH',KEYS[1],math.random())
i = i-1
end
return res
EOF
r.del(:mylist)
puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))
여기서 하는 일은 PRNG의 시드를 인자 중 하나로 보내는 거예요. 스크립트 출력은 같은 인자에 대해 항상 같지만(우리의 요구사항), 호출마다 인자 중 하나를 바꿔 클라이언트 측에서 랜덤 시드를 생성해요. 이 시드는 복제 링크와 Append Only File 모두에서 인자 중 하나로 전파되므로, AOF를 재로드하거나 replica가 스크립트를 처리할 때 같은 변경이 생성된다는 걸 보장해요.
참고: 이 동작의 중요한 부분은 Redis가 math.random과 math.randomseed로 구현하는 PRNG가 Redis를 실행하는 시스템의 아키텍처와 무관하게 같은 출력을 내는 게 보장된다는 거예요. 32비트·64비트, big-endian·little-endian 시스템 모두 같은 출력을 만들어요.
Eval 스크립트 디버깅
Redis 3.2부터 Redis는 네이티브 Lua 디버깅을 지원해요. Redis Lua 디버거는 서버(Redis 자신)와 기본적으로 redis-cli인 클라이언트로 구성된 원격 디버거예요. Lua 디버거는 Redis 문서의 Lua scripts debugging 섹션에 설명돼 있어요.
낮은 메모리 조건에서의 실행
Redis의 메모리 사용량이 maxmemory 한도를 초과하면, 추가 메모리를 사용하는 스크립트의 첫 번째 쓰기 명령은 스크립트를 중단시켜요(redis.pcall을 사용하지 않은 경우). 단 예외는 첫 write 명령이 추가 메모리를 쓰지 않을 때인데, 예를 들어 DEL, LREM이 그래요. 그 경우 Redis는 원자성을 보장하기 위해 스크립트의 모든 명령을 실행하도록 허용해요. 이후 쓰기가 추가 메모리를 소비하면 maxmemory 설정 임계값을 넘을 수 있어요.
또 다른 시나리오는 Redis가 maxmemory보다 약간 아래일 때 실행을 시작해서 스크립트의 첫 번째 쓰기 명령이 허용되는 경우예요. 스크립트가 실행되며 이후 쓰기 명령이 더 많은 메모리를 소비해 서버가 maxmemory 지시어보다 많은 RAM을 쓰게 돼요.
이런 시나리오에서는 maxmemory-policy 설정 지시어를 noeviction 외의 값으로 설정하는 걸 고려하세요. 그리고 Lua 스크립트는 실행 사이에 eviction이 작동할 수 있도록 최대한 빨라야 해요. flags를 사용해 이 동작을 바꿀 수도 있어요.
Eval flags
보통 Eval 스크립트를 실행할 때 서버는 스크립트가 데이터베이스에 어떻게 접근하는지 알지 못해요. 기본적으로 Redis는 모든 스크립트가 데이터를 읽고 쓴다고 가정해요. 하지만 Redis 7.0부터 스크립트를 만들 때 플래그를 선언해 Redis에 어떻게 동작해야 하는지 알려줄 수 있어요.
방법은 스크립트 첫 줄에 Shebang 문을 쓰는 거예요:
#!lua flags=no-writes,allow-stale
local x = redis.call('get','x')
return x
Redis가 #! 주석을 보는 순간, 스크립트가 플래그를 선언하는 것처럼 취급한다는 점에 주의하세요. 플래그가 정의되지 않았어도 #! 줄이 없는 스크립트와는 다른 기본값 집합을 갖게 돼요.
또 다른 차이점은 #!가 없는 스크립트는 서로 다른 클러스터 hash slot에 속한 키에 접근하는 명령을 실행할 수 있지만, #!가 있는 스크립트는 기본 플래그를 상속받으므로 그렇게 할 수 없어요.
다양한 스크립트 플래그와 기본값에 대해서는 Script flags를 참고하세요.