요약
Redis로 bulk insert할 방법을 찾아보다가 pipeline을 알게되었고, 메서드 수행시간을 기준으로 성능을 테스트 해봤는데 효과가 분명히 있어 CacheModule에 pipeline을 사용하는 bulk처리 메서드를 추가했습니다.
직접 캐시 모듈 을 만들다보니, 여러 개의 항목을 저장하거나 삭제하는 메서드 또한 필요함을 느꼈습니다. 레디스의 hash자료구조에선 하나의 key에 속한 여러개의 hashKey와 value를 map 형식으로 전달해 여러 항목들을 저장하는 메서드를 제공했습니다. 하지만 value(string) 자료구조에선 여러개의 value를 저장하는 방식을 제공하지 않았고, 만약 저장해야하는 대상이 수백 수천개가 되면, 그만큼 레디스와의 RTT로인해 지연이 많이 발생되며, 요청 자체가 많아져서 병목이 발생할 것이라 생각했습니다.
MySQL처럼 bulk insert를 지원하는 방법을 찾아보았지만 레디스엔 bulk insert가 존재하지 않았고, 대신 pipeline을 통해 여러 명령어를 한번의 요청으로 보낼 수 있음을 알게되었습니다.
RedisTemplate의 executePipleined 메서드를 이용해 파이프라인에 들어갈 명령어를 정의해주었습니다. RedisConnection을 사용하는 방식은 레디스에 직접 접근하는 방식이라 RedisTemplate에서 key와 value 각각의 Serializer를 추출해 key와 value를 직접 직렬화해주었습니다.
// 여러 개의 set 연산을 1번의 트랜잭션으로 처리
public <K, V> void putAll(CacheType cacheType, List<V> values, Function<V, K> keyExtractor){
redisTemplate.executePipelined((RedisCallback<Object>) RedisConnection -> {
values.forEach(item -> {
String key = getCacheKey(cacheType, keyExtractor.apply(item));
RedisConnection.set(keySerializer.serialize(key), valueSerializer.serialize(item));
});
return null;
});
}
아래와 같이, 직접 일일이 put해주는 방식과 piepeline을 사용해 한번에 put 명령어를 보내는 방식을, 필드가 5개인 객체를 저장해보며 수행시간을 기준으로 성능을 테스트해보았습니다.
@Test
@DisplayName("각각")
public void writeAllManuallyObjectTest(){
entities.forEach(entity -> cacheModule.put(CacheType.TEST, entity.getId(), entity));
}
@Test
@DisplayName("파이프라인")
public void writeAllPipelinedObjectTest(){
cacheModule.putAll(CacheType.TEST, entities, e -> e.getId());
}
테스트 결과, 10개 정도의 적은 데이터를 저장할 때도 파이프라인이 빠른 성능을 낼 수 있음을 볼 수 있었습니다. 물론 파이프라인을 적용하는 것은 테스트로 성능향상을 꼭 확인하고 적용해야겠지만, 이정도면 꽤 긍정적으로 고려해볼만 한 것 같습니다.
저장 갯수 | 각각 저장 | 파이프라인 |
---|---|---|
10개 | 34ms | 20ms |
100개 | 86ms | 27ms |
1,000개 | 285ms | 34ms |
2,000개 | 254ms | 44ms |
5,000개 | 543ms | 128ms |
10,000개 | 803ms | 127ms |
30,000개 | 2078ms | 321ms |
저장되는 객체가 무엇인지, 레디스 서버의 상태 등에 따라 성능이 달라질 수 있습니다. 그래서 일단은 CacheModule의 모든 bulk 연산을 pipeline화 하지 않고, CacheModule에 pipeline 전용 bulk 삽입 및 삭제 메서드를 추가했습니다. 그래서 필요한 부분에 신중히 도입할 수 있게 했습니다.