목표
RedisTemplate 과 CrudRepository 의 속도 차이를 확인해 본다.
결과
Key-Value DB의 추가적인 기능을 쓰지 않고 단순한 캐시용도로만 쓴다면 속도가 빠른 RedisTemplate을 쓰는게 좋아보인다.
RedisTemplate 사용 시에는 Operation 을 반환하여 해당 Operation을 통해 데이터를 redis에 저장할 수 있게 된다. 예를 들어 Hash 객체를 사용하는 경우, HashOperations
를 반환하여 해당 Operation 의 메서드를 사용하게 되는데, 이는 Redis에 저장하는 로직 밖에 없어 간단하다.
RedisTemplate 저장 실제 코드
@Override
public void put(K key, HK hashKey, HV value) {
byte[] rawKey = rawKey(key);
byte[] rawHashKey = rawHashKey(hashKey);
byte[] rawHashValue = rawHashValue(value);
execute(connection -> {
connection.hSet(rawKey, rawHashKey, rawHashValue);
return null;
});
}
해당 라이브러리는 캐시용도로 쓰기엔 추상화가 매우 많이 되어있고 옵션도 많다. 따라서 진행되는 과정도 단순하지 않다.
처음 save 메서드를 호출하면 insert와 update 분기를 나누는 작업이 있는데, Redis 객체의 경우엔 @RedisHash
어노테이션으로 이미 객체의 id
가 있어 update 로 판단되어 진행된다. 그리고 update 에서는 adapter에 put 메서드를 요청하게 되며, KeySpace 조정작업을 거치게 된다.
KeySpace 는 단순 조회, 저장용도가 아닌 SecondIndex 처럼 옵션 기능을 위한 Index Set 이다.
CrudRepository 저장 실제 코드 1
@Override
public <T> T update(Object id, T objectToUpdate) {
Assert.notNull(id, "Id for object to be inserted must not be null");
Assert.notNull(objectToUpdate, "Object to be updated must not be null");
String keyspace = resolveKeySpace(objectToUpdate.getClass());
potentiallyPublishEvent(KeyValueEvent.beforeUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate));
Object existing = execute(adapter -> adapter.put(id, objectToUpdate, keyspace));
potentiallyPublishEvent(
KeyValueEvent.afterUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate, existing));
return objectToUpdate;
}
그리고 중간의 adapter
라는 녀석이 put 메서드를 호출하는 것을 확인할 수 있는데, 해당 put은 RedisTemplate 의 put 과 다르다. 밑에 실제코드가 존재하고 이런 코드를 통해서 여러가지 작업을 많이 하게 된다. 이렇게 명령어가 전달되는 수 자체가 put 하나를 위해 딸려오게 된다.
CrudRepository 저장 실제 코드 2
@Override
public Object put(Object id, Object item, String keyspace) {
RedisData rdo = item instanceof RedisData ? (RedisData) item : new RedisData();
if (!(item instanceof RedisData)) {
converter.write(item, rdo);
}
if (ObjectUtils.nullSafeEquals(EnableKeyspaceEvents.ON_DEMAND, enableKeyspaceEvents)
&& this.expirationListener.get() == null) {
if (rdo.getTimeToLive() != null && rdo.getTimeToLive() > 0) {
initKeyExpirationListener();
}
}
if (rdo.getId() == null) {
rdo.setId(converter.getConversionService().convert(id, String.class));
}
redisOps.execute((RedisCallback<Object>) connection -> {
byte[] key = toBytes(rdo.getId());
byte[] objectKey = createKey(rdo.getKeyspace(), rdo.getId());
boolean isNew = connection.del(objectKey) == 0;
connection.hMSet(objectKey, rdo.getBucket().rawMap());
if (isNew) {
connection.sAdd(toBytes(rdo.getKeyspace()), key);
}
if (expires(rdo)) {
connection.expire(objectKey, rdo.getTimeToLive());
}
if (keepShadowCopy()) { // add phantom key so values can be restored
byte[] phantomKey = ByteUtils.concat(objectKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX);
if (expires(rdo)) {
connection.del(phantomKey);
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
connection.expire(phantomKey, rdo.getTimeToLive() + PHANTOM_KEY_TTL);
} else if (!isNew) {
connection.del(phantomKey);
}
}
IndexWriter indexWriter = new IndexWriter(connection, converter);
if (isNew) {
indexWriter.createIndexes(key, rdo.getIndexedData());
} else {
indexWriter.deleteAndUpdateIndexes(key, rdo.getIndexedData());
}
return null;
});
return item;
}
같은 환경으로 Redis 속도 비교를 해보았다. 평균적인 속도를 기준으로 나타내었고, RedisTemplate의 속도가 훨씬 빠름을 알 수 있었다.
저장 | 조회(데이터 없을 경우) | 조회(데이터 있을 경우) | |
---|---|---|---|
RedisTemplate | 5ms | 3ms | 3ms |
CrudRepository | 45ms | 3ms | 13ms |
CrudRepository 의 속도가 느린 이유