Redis 배열
Redis 배열 (Redis arrays) (arrays-2)
Redis 배열은 희소(sparse)하고 인덱스로 접근할 수 있는 데이터 구조예요. 정수 인덱스(0부터 2⁶⁴−1 범위)를 문자열 값에 매핑하죠. 리스트와 달리 요소를 시퀀스의 위치가 아니라 인덱스로 직접 접근하고, 점유되지 않은 사이의 빈칸을 할당하지 않고도 어떤 인덱스든 설정할 수 있어요. 그래서 타임스탬프 이벤트 로그, 스트리밍 측정값 위의 링 버퍼, 슬라이딩 윈도우 분석처럼 희소하거나 높은 인덱스 접근 패턴을 다루는 작업에 아주 잘 맞아요.
이 페이지에서는 배열의 기본 사용법부터 길이 측정, 범위 읽기, 순차 삽입, 링 버퍼 모드, 성능까지 하나씩 옆에서 설명드릴게요.
기본 사용법 (Basic usage)
ARSET으로 특정 인덱스부터 시작하는 연속된 값들을 하나 이상 쓰고, ARGET으로 한 인덱스의 값을 읽어요. 설정되지 않은 인덱스에 접근하면 nil이 응답으로 돌아옵니다.
res1 = r.arSet("events:1", 0, "login", "click", "purchase")
print(res1) # >>> 3
res2 = r.arGet("events:1", 0)
print(res2) # >>> login
res3 = r.arGet("events:1", 999)
print(res3) # >>> None
임의의, 연속되지 않은 인덱스에 값을 쓰려면 ARMSET을 사용해요. 한 번의 왕복(round trip)으로 여러 인덱스를 읽으려면 ARMGET을 쓰면 돼요.
# (참고) Node.js 예시
const mSetResult = await client.arMSet('metrics', { 0: '10', 5: '20', 100: '30' });
console.log(mSetResult); // >>> 3
const mGetResult = await client.arMGet('metrics', [0, 5, 100, 999]);
console.log(mGetResult); // >>> [ '10', '20', '30', null ]
배열 길이 vs 요소 수 (Array length vs. element count)
Redis 배열은 서로 다른 두 가지 크기 측정값을 노출해요.
희소 배열에서는 이 두 값이 상당히 달라질 수 있어요.
res6 = r.arSet("sparse", 0, "a")
print(res6) # >>> 1
res7 = r.arSet("sparse", 1000000, "b")
print(res7) # >>> 1
res8 = r.arLen("sparse")
print(res8) # >>> 1000001
res9 = r.arCount("sparse")
print(res9) # >>> 2
설정된 값은 인덱스 0과 1000000 두 개뿐인데, 논리적 길이는 1000001이 나와요. 이 차이가 희소 배열의 핵심 특징이죠.
범위 읽기 (Reading ranges)
ARGETRANGE는 범위 안의 모든 위치를 인덱스 순서대로 반환해요. 빈 슬롯은 nil로 돌아오죠. start와 end를 뒤집으면 방향도 뒤집힙니다.
const rangeSetResult = await client.arMSet('seq', { 0: 'a', 1: 'b', 3: 'd' });
console.log(rangeSetResult); // >>> 3
const rangeResult = await client.arGetRange('seq', 0, 3);
console.log(rangeResult); // >>> [ 'a', 'b', null, 'd' ]
존재하는 요소만 순회하면서 인덱스와 값을 함께 가져오려면 ARSCAN을 써요. 빈 슬롯을 건너뛰고 인덱스-값이 번갈아 나오는 평면(flat) 리스트를 반환하며, 선택적으로 LIMIT로 결과 크기를 제한할 수 있어요.
const scanResult = await client.arScan('seq', 0, 3);
for (const { index, value } of scanResult) {
console.log(`${index} -> ${value}`);
}
// >>> 0 -> a
// >>> 1 -> b
// >>> 3 -> d
순차 삽입 (Sequential insertion)
ARINSERT는 호출할 때마다 자동으로 앞으로 나아가는 내부 커서(cursor)를 사용해 값을 추가해요. ARNEXT로 다음 삽입 위치를 확인하고, ARSEEK으로 커서를 다시 배치할 수 있어요.
res14 = r.arInsert("log", "event1")
print(res14) # >>> 0
res15 = r.arInsert("log", "event2")
print(res15) # >>> 1
res16 = r.arNext("log")
print(res16) # >>> 2
res17 = r.arSeek("log", 10)
print(res17) # >>> 1
res18 = r.arInsert("log", "event3")
print(res18) # >>> 10
링 버퍼 모드 (Ring buffer mode)
ARRING은 배열을 고정 크기의 원형 버퍼로 바꿔요. 호출할 때마다 insert_idx % size 위치에 값을 넣고, 창(window)이 가득 차면 인덱스 0으로 되돌아가 가장 오래된 항목을 덮어씁니다.
res19 = r.arRing("readings", 3, "v0")
print(res19) # >>> 0
res20 = r.arRing("readings", 3, "v1")
print(res20) # >>> 1
res21 = r.arRing("readings", 3, "v2")
print(res21) # >>> 2
res22 = r.arRing("readings", 3, "v3")
print(res22) # >>> 0
res23 = r.arGet("readings", 0)
print(res23) # >>> v3
내부 구조 살펴보기 (Introspection)
ARINFO는 배열 내부 구조에 대한 메타데이터(논리 길이, 요소 수, 다음 삽입 인덱스 등)를 반환해요. FULL 옵션을 넘기면 슬라이스별 통계(채움 비율, dense vs sparse 슬라이스 수 등)까지 포함해서 돌려줍니다.
> ARINFO readings
1) "len"
2) (integer) 3
3) "count"
4) (integer) 3
5) "next-insert-index"
6) (integer) 0
...
설정 (Configuration)
배열 동작에 영향을 주는 설정 파라미터는 다음과 같아요.
array-slice-sizearray-sparse-kmaxarray-sparse-kmin
자세한 내용은 Redis 설정 페이지를 참고하세요.
성능 (Performance)
대부분의 배열 명령은 O(1) 복잡도예요. ARSET, ARGET, ARDEL, ARINSERT, ARNEXT, ARSEEK, ARCOUNT, ARLEN이 여기 해당돼요.
N개 요소를 건드리는 연산(예: ARGETRANGE, ARSCAN, ARDELRANGE, AROP, ARLASTITEMS)은 O(N)이에요. 내부의 슬라이스 배열 인코딩은 dense와 sparse 접근 패턴을 모두 효율적으로 처리해서, 큰 인덱스 간격도 메모리를 거의 소모하지 않아요.
대안 (Alternatives)
배열은 다른 Redis 컬렉션 타입을 대체하기보다 보완하는 역할을 해요.
- 양 끝에서 push/pop이 필요하거나, 기존 요소 사이에 요소를 끼워 넣어야 한다면 Redis 리스트를 사용하세요.
- 값을 숫자 인덱스가 아니라 필드 이름으로 접근해야 한다면 Redis 해시를 사용하세요.
- 소비자 그룹과 확인(acknowledgement)을 지원하는 append-only 이벤트 로그가 필요하다면 Redis 스트림을 사용하세요.
제한 사항 (Limits)
ARGETRANGE는 우연히 너무 큰 범위를 읽는 것을 막기 위해 호출당 최대 1,000,000개 요소라는 하드 리미트를 적용해요.