Redis 해시
Redis 해시 (Redis Hashes)
Redis 해시(hash)는 필드-값(field-value) 쌍의 컬렉션으로 구조화된 레코드 타입이에요. 기본 객체를 표현하거나 카운터 그룹을 저장하는 등 다양한 용도로 쓸 수 있죠. 해시는 배우기 쉽고 활용도가 높은 데이터 타입이라, 옆에서 차근차근 설명해 드릴게요.
기본 사용법: HSET과 HGET (Foundational: HSET and HGET)
HSET으로 해시 필드를 설정하고 HGET으로 조회할 수 있어요. 같은 필드에 다시 설정하면 기존 값을 덮어써요.
res1 = r.hset(
"bike:1",
mapping={
"model": "Deimos",
"brand": "Ergonom",
"type": "Enduro bikes",
"price": 4972,
},
)
print(res1)
# >>> 4
res2 = r.hget("bike:1", "model")
print(res2)
# >>> 'Deimos'
res3 = r.hget("bike:1", "price")
print(res3)
# >>> '4972'
res4 = r.hgetall("bike:1")
print(res4)
# >>> {'model': 'Deimos', 'brand': 'Ergonom', 'type': 'Enduro bikes', 'price': '4972'}
const res1 = await client.hSet(
'bike:1',
{
'model': 'Deimos',
'brand': 'Ergonom',
'type': 'Enduro bikes',
'price': 4972,
}
)
console.log(res1) // 4
const res2 = await client.hGet('bike:1', 'model')
console.log(res2) // 'Deimos'
const res3 = await client.hGet('bike:1', 'price')
console.log(res3) // '4972'
const res4 = await client.hGetAll('bike:1')
console.log(res4)
hashFields := []string{
"model", "Deimos",
"brand", "Ergonom",
"type", "Enduro bikes",
"price", "4972",
}
res1, err := rdb.HSet(ctx, "bike:1", hashFields).Result()
if err != nil {
panic(err)
}
fmt.Println(res1) // >>> 4
res2, err := rdb.HGet(ctx, "bike:1", "model").Result()
if err != nil {
panic(err)
}
fmt.Println(res2) // >>> Deimos
res3, err := rdb.HGet(ctx, "bike:1", "price").Result()
if err != nil {
panic(err)
}
fmt.Println(res3) // >>> 4972
cmdReturn := rdb.HGetAll(ctx, "bike:1")
res4, err := cmdReturn.Result()
if err != nil {
panic(err)
}
fmt.Println(res4)
// >>> map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes]
Redis 해시는 객체를 표현하기에 편리하지만, 사실 해시 안에 넣을 수 있는 필드 수에는 (사용 가능한 메모리를 제외하면) 실질적인 제한이 없어요. 그래서 애플리케이션 안에서 해시를 다양한 방식으로 활용할 수 있죠.
HSET은 해시의 여러 필드를 설정하고, HGET은 단일 필드를 조회해요. HMGET은 HGET과 비슷하지만 값의 배열을 반환해요. 서버 왕복(round trip)을 줄여야 할 때 HMGET으로 여러 필드 값을 한 번에 가져올 수 있어요.
# bike:1 해시를 재생성해 이 예제가 단독으로 실행되게 해요.
r.delete("bike:1")
r.hset(
"bike:1",
mapping={
"model": "Deimos",
"brand": "Ergonom",
"type": "Enduro bikes",
"price": 4972,
},
)
res5 = r.hmget("bike:1", ["model", "price"])
print(res5)
# >>> ['Deimos', '4972']
개별 필드에 연산을 수행하는 명령어도 있어요. 예를 들어 HINCRBY로 필드 값을 증가시킬 수 있어요(필드가 없으면 생성하고 0으로 초기화해요).
# bike:1 해시를 재생성해 이 예제가 단독으로 실행되게 해요.
r.delete("bike:1")
r.hset(
"bike:1",
mapping={
"model": "Deimos",
"brand": "Ergonom",
"type": "Enduro bikes",
"price": 4972,
},
)
res6 = r.hincrby("bike:1", "price", 100)
print(res6)
# >>> 5072
res7 = r.hincrby("bike:1", "price", -100)
print(res7)
# >>> 4972
전체 해시 명령 목록은 문서의 해시 명령 참조에서 확인하세요.
참고: 작은 해시(즉, 값이 작은 요소 몇 개)는 메모리에서 특별한 방식으로 인코딩되어 매우 메모리 효율적이에요.
예시 (Examples)
bike:1이 탄 횟수, 사고 난 횟수, 주인(owner)이 바뀐 횟수를 카운터로 저장해 보세요. HINCRBY와 HMGET을 조합하면 여러 필드에 걸친 원자적 업데이트가 필요할 때 실용적인 패턴이 돼요.
res11 = r.hincrby("bike:1:stats", "rides", 1)
print(res11)
# >>> 1
res12 = r.hincrby("bike:1:stats", "rides", 1)
print(res12)
# >>> 2
res13 = r.hincrby("bike:1:stats", "rides", 1)
print(res13)
# >>> 3
res14 = r.hincrby("bike:1:stats", "crashes", 1)
print(res14)
# >>> 1
res15 = r.hincrby("bike:1:stats", "owners", 1)
print(res15)
# >>> 1
res16 = r.hget("bike:1:stats", "rides")
print(res16)
# >>> 3
res17 = r.hmget("bike:1:stats", ["crashes", "owners"])
print(res17)
# >>> ['1', '1']
res8, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res8) // >>> 1
res9, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res9) // >>> 2
res10, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res10) // >>> 3
res11, err := rdb.HIncrBy(ctx, "bike:1:stats", "crashes", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res11) // >>> 1
res12, err := rdb.HIncrBy(ctx, "bike:1:stats", "owners", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res12) // >>> 1
res13, err := rdb.HGet(ctx, "bike:1:stats", "rides").Result()
if err != nil {
panic(err)
}
fmt.Println(res13) // >>> 3
res14, err := rdb.HMGet(ctx, "bike:1:stats", "crashes", "owners").Result()
if err != nil {
panic(err)
}
fmt.Println(res14) // >>> [1 1]
필드 만료 (Field expiration)
Redis 7.4는 개별 해시 필드에 만료 시간 또는 TTL(time-to-live) 값을 지정하는 기능을 도입했어요. 이 기능은 키 만료와 비슷하며 여러 유사한 명령을 포함해요.
특정 필드에 정확한 만료 시간이나 TTL 값을 설정하는 명령:
HEXPIRE: 남은 TTL을 초(seconds) 단위로 설정HPEXPIRE: 남은 TTL을 밀리초(milliseconds) 단위로 설정HEXPIREAT: 만료 시간을 초 단위 타임스탬프로 설정HPEXPIREAT: 만료 시간을 밀리초 단위 타임스탬프로 설정
특정 필드가 만료되는 정확한 시간 또는 남은 TTL을 조회하는 명령:
HEXPIRETIME: 만료 시간을 초 단위 타임스탬프로 조회HPEXPIRETIME: 만료 시간을 밀리초 단위 타임스탬프로 조회HTTL: 남은 TTL을 초 단위로 조회HPTTL: 남은 TTL을 밀리초 단위로 조회
특정 필드의 만료를 제거하는 명령:
HPERSIST: 만료 제거
Redis 8.0은 다음 명령을 도입했어요:
HGETEX: 주어진 해시 키의 하나 이상 필드 값을 가져오고 선택적으로 만료 시간 또는 TTL 설정HSETEX: 주어진 해시 키의 하나 이상 필드 값을 설정하고 선택적으로 만료 시간 또는 TTL 설정
일반적인 필드 만료 사용 사례 (Common field expiration use cases)
- 이벤트 추적 (Event Tracking): 해시 키로 지난 한 시간의 이벤트를 저장하고 각 이벤트의 TTL을 한 시간으로 설정.
HLEN으로 지난 시간의 이벤트 수를 집계. - 사기 감지 (Fraud Detection): 이벤트에 대한 시간별 카운터가 있는 해시를 만들고 각 필드의 TTL을 48시간으로 설정. 해시를 조회해 지난 48시간의 시간별 이벤트 수를 얻음.
- 고객 세션 관리 (Customer Session Management): 고객 데이터를 해시 키에 저장. 각 세션마다 새 해시 키를 만들고 세션 필드를 고객의 해시 키에 추가. 세션이 만료되면 세션 키와 고객 해시 키의 세션 필드가 모두 자동으로 만료되게 함.
- 활성 세션 추적 (Active Session Tracking): 모든 활성 세션을 해시 키에 저장. 각 세션의 TTL이 비활성 후 자동으로 만료되도록 설정.
HLEN으로 활성 세션 수를 집계.
필드 만료 예시 (Field expiration examples)
해시 필드 만료는 공식 클라이언트 라이브러리에서 지원돼요. 아래 예시는 센서 데이터를 저장하는 해시로 필드 만료 명령을 보여줘요.
| 필드 | 값 |
|---|---|
air_quality |
256 |
battery_level |
89 |
필드가 만료되므로 각 예시는 sensor:sensor1 해시를 먼저 재생성해 단독으로 실행되게 해요.
해시의 두 필드에 60초 TTL을 설정하고 남은 TTL을 조회:
r.delete("sensor:sensor1")
r.hset("sensor:sensor1", mapping={"air_quality": 256, "battery_level": 89})
# 해시의 두 필드에 60초 TTL 설정
res18 = r.hexpire("sensor:sensor1", 60, "air_quality", "battery_level")
print(res18)
# >>> [1, 1]
# 해당 필드의 남은 TTL 조회
res19 = r.httl("sensor:sensor1", "air_quality", "battery_level")
print(res19)
# >>> [60, 60]
# (실제 값은 약간 낮을 수 있어요)
해시 필드의 TTL을 밀리초로 설정하고 남은 TTL을 밀리초로 조회:
r.delete("sensor:sensor1")
r.hset("sensor:sensor1", mapping={"air_quality": 256, "battery_level": 89})
# 'air_quality' 필드의 TTL을 밀리초 단위로 설정
res20 = r.hpexpire("sensor:sensor1", 60000, "air_quality")
print(res20)
# >>> [1]
# 남은 TTL을 밀리초 단위로 조회
res21 = r.hpttl("sensor:sensor1", "air_quality")
print(res21)
# >>> [59994]
# (실제 값은 다를 수 있어요)