레거시 프로젝트를 이관하면서, 레디스 에서 key-value 외에 다양한 자료구조가 있다는 것을 알았다. 자료구조와 메서드 그리고 어떤 상황에 해당 자료구조가 적절한지 알아보자.
레디스는 싱글 스레드 기반으로 동작하기 때문에 명령어의 시간복잡도를 파악한 후 해당 명령어로 인한 블로킹이 얼마나 생기는지 파악 후, 작업을 진행해야 한다. 특히 N이 데이터베이스의 키 갯수일 때 실행속도가 O(N)인 KEYS *
를 운영에서 날리는 작업은 피해야 한다.
Strings는 key-value 형태의 바이트 시퀀스를 저장하는 자료구조이다.
> SET key value
> SET member:coupon:1234 1
OK
public void setStringValue(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
> GET key
> GET member:coupon:1234
"1"
public String getStringValue(String key) {
return redisTemplate.opsForValue().get(key);
}
> SET key 0
> SET coupon_cnt 0
OK
> INCR coupon_cnt
(integer) 1
> INCRBY coupon_cnt 10
(integer) 11
public long increment(String key) {
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
return valueOps.increment(key);
}
public long incrementBy(String key, long incrementValue) {
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
return valueOps.increment(key, incrementValue);
}
INCR명령은 문자열 값을 정수로 구문 분석하고 1씩 증가시킨 후 마지막으로 얻은 값을 새 값으로 설정한다. INCRBY, DECR및 와 같은 다른 유사한 명령이 있고, 내부적으로는 약간 다른 방식으로 작동한다.
INCR이 원자적이라는 것은 동일한 키에 대해 INCR을 발행하는 여러 클라이언트라도 결코 경쟁 조건에 빠지지 않는다. 예를 들어, 클라이언트 1이 "10"을 읽고, 클라이언트 2가 "10"을 동시에 읽고, 둘 다 11로 증가하고 새 값을 11로 설정하는 일은 결코 발생하지 않는다. 최종 값은 항상 12이고 read-increment-set 작업은 다른 모든 클라이언트가 동시에 명령을 실행하지 않는 동안 수행된다.
Lists 의 다른 메서드가 필요하다면 공식문서에 찾아보자.
Lists는 문자열 값의 연결된 목록을 저장하는 자료구조이다.
> LPUSH key value
> LPUSH member:coupon:1234:read 20231107
(integer) 1
> LPUSH member:coupon:1234:read 20231108
(integer) 2
public void leftPushToList(String key, String value) {
redisTemplate.opsForList().leftPush(key, value);
}
public void rightPushToList(String key, String value) {
redisTemplate.opsForList().rightPush(key, value);
}
> LRANGE key start end
> LRANGE member:coupon:1234:read 0 10
1) "20231108"
2) "20231107"
public List<String> getRangeFromList(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
public String getListItemAtIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
Lists 의 다른 메서드가 필요하다면 공식문서에 찾아보자.
Hashes는 필드-값 쌍의 컬렉션으로 구성된 자료구조이다.
> HSET key field value
(integer) 1
> HSET member:coupon:1234:read:20231108 1123 1
(integer) 1
> HSET member:coupon:1234:read:20231108 4123 1
(integer) 1
public void hset(String key, String field, String value) {
redisTemplate.opsForHash().put(key, subKey, value);
}
> HGET key field
> HGET member:coupon:1234:read:20231108 1123
"1"
public Boolean hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
Hashes 의 다른 메서드가 필요하다면 공식문서에 찾아보자.
> EXPIRE key value
> EXPIRE member:coupon:1234 100
(integer) 1
public void setExpiryForRedisKey(String key, long ttl) {
redisTemplate.expire(key, ttl, TimeUnit.SECONDS);
}
> TTL key
> TTL member:coupon:1234
(integer) 100
public Long getExpiryForRedisKey(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
> DEL key
> UNLINK key
public void deleteKey(String key) {
redisTemplate.delete(key);
}
public void unlink(String key) {
redisTemplate.unlink(key);
}
저장이 필요할 때마다 커넥션을 맺을 필요없이 한번 커넥션에서 많은 양의 데이터를 저장하여 RTT 및 부하를 줄일 수 있다.
public void bulk(Map<String, Long> keyTtlMap, String value) {
redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
keyTtlMap.forEach((key, ttl) -> {
stringRedisConn.set(key, value, Expiration.from(ttl, TimeUnit.SECONDS), RedisStringCommands.SetOption.UPSERT);
});
return null;
});
}
첫번째 그림은 이는 TCP연결을 매번해야 하는 오버헤드가 있어 Keep Alive속성이 생겼다.
두번째 그림은 Http의 Keep Alive속성때문에 연결이 지속되어 리퀘스트를 보내고 리스폰스를 받는다.
세번째 그림은 한번에 수많은 리퀘스트를 보내고 리스폰스를 기다리지 않고 보낼 수 있다.
출처