아직 Redis에 대해서는 잘모르지만, 싱글 스레드로 동작하기 때문에 O(N)은 피해야 한다는 것을 알게 되었다.
redis-cli가 아닌 Spring Data Redis의 RedisTemplate에서 어떻게 대체할 수 있는지 어떻게 대체 했는지 기록해보려고 한다.
keys 명령어 → Scan 명령어로 개선하기
keys 사용할 경우
@Transactional
public void updateRDB() {
HashOperations<String, String, Integer> hashOperations = redisTemplate.opsForHash();
Set<String> keys = redisTemplate.keys("posts:*");
for (String key : keys) {
int index = key.indexOf(":");
Long postId = Long.valueOf(key.substring(index + 1));
RedisHits hits = getHits(postId);
System.out.println("key = " + key.substring(index + 1));
Post post = postRepository.findById(postId).get();
post.increaseHits(hits.getCount());
}
flushAll();
}
- hash 자료구조를 사용하였기 때문에 opsForhash()를 사용함.
- Redis에 조회수를 캐싱하고, 한 번에 조회수를 RDB로 업데이트 하기위해 사용함.
- 그 과정에서 모든 키를 알아야 모두 RDB로 업데이트가 가능하기 때문에 keys를 사용함.
- 데이터가 적기 때문에 전혀 문제는 없음.
- 하지만 Redis 도입 이유가 캐싱하여 성능 향상이 있었기 때문에 가능한 처음부터 성능을 고려하여 작성해야 한다고 생각함.
scan 사용
@Transactional
@Override
public void updateRDB() {
ScanOptions scanOptions = ScanOptions.scanOptions().match("*").count(10).build();
Cursor<byte[]> keys = redisTemplate.getConnectionFactory().getConnection().scan(scanOptions);
while (keys.hasNext()) {
Long postId = extractPostId(keys);
Post post = postRepository.findById(postId).get();
post.increaseHits(getHits(postId).getCount());
}
flushAll();
}
private Long extractPostId(Cursor<byte[]> keys) {
String key = new String(keys.next());
int index = key.indexOf(":");
return Long.valueOf(key.substring(index + 1));
}
- 크게 다른 부분은 없고 scan()명령어만 사용했다.
- count()는 한 번에 몇개의 key를 조회할지 정하는 옵션인데 redis-cli에서 기본이 10개라서
일단 10개로 했다. 실제 사용해보면서, 정해야할 부분이라고 생각했다.
Keys와 Scan 차이점 알아보기
keys는 O(N)으로 모든 키를 한 번에 가져온다. 즉, 뒤에 있는 패킷은 지연이 발생한다.
반면, scan은 짧은 여러번의 명령으로 대체할 수 있다.
scan을 다시 명령하는 찰나의 순간에 다른 작업이 수행될 수 있기 때문이다.
scan은 cursor 방식으로 다음 cursor를 지정하게 되고, 10개씩(기본값) key를 조회하게 된다.
다음 cursor가 없으면 cursor는 0을 반환한다.
RedisTemplate의 동작 방식 간단하게 알아보기
직접 cli로 명령어를 사용하면 위 사진과 같이 scan [cursorId]로 key목록을 count만큼 조회하게 된다.
RedisTemplate
에서는 어떻게 될까?테스트는 아래와 같이 4개의 post를 redis에 저장하고,
updateRDB()
메서드를 실행하였다.
이렇게 한 이유는 scan을 어떻게 사용하는지 알아보기 위함이다.
위에 Scan을 적용한 코드를 기반으로 한다.@Test void redisTemplateScan_내부_로직_확인() { // TODO : RDB 반영 시 삭제할 경우 동시성 테스트 필요 // given Member member = new Member(NAVER_EMAIL); memberRepository.save(member); Long postId1 = postRepository.save(new Post("포스트1제목", "포스트1내용", member)).getId(); Long postId2 = postRepository.save(new Post("포스트2제목", "포스트2내용", member)).getId(); Long postId3 = postRepository.save(new Post("포스트2제목", "포스트2내용", member)).getId(); Long postId4 = postRepository.save(new Post("포스트2제목", "포스트2내용", member)).getId(); hitsRedisRepository.increaseHits(postId1); hitsRedisRepository.increaseHits(postId2); hitsRedisRepository.increaseHits(postId3); hitsRedisRepository.increaseHits(postId4); // when hitsRedisRepository.updateRDB(); // then assertThat(hitsRedisRepository.getHits(postId1)).isNull(); assertThat(hitsRedisRepository.getHits(postId2)).isNull(); assertThat(hitsRedisRepository.getHits(postId3)).isNull(); assertThat(hitsRedisRepository.getHits(postId4)).isNull(); assertThat(postRepository.findById(postId1).get().getHits()).isEqualTo(1); assertThat(postRepository.findById(postId2).get().getHits()).isEqualTo(1); assertThat(postRepository.findById(postId3).get().getHits()).isEqualTo(1); assertThat(postRepository.findById(postId4).get().getHits()).isEqualTo(1); }
먼저
Cursor<byte[]> keys = redisTemplate.getConnectionFactory().getConnection().scan(scanOptions)
을 확인 해보자.LettuceScanCursor.java
에서 지정한 count, pattern을 사용한다.
super
인ScanCursor.java
를 보면 첫 scan이기 때문에 cursorId를 0번 부터 탐색한다.cli
로 보면scan 0
과 같은 것이다.
즉, keys에는 두 개 키가 담겨있고,
next cursorId
가 담겨있는 것이다.cursorId
는 2가 담겨있다.
keys.hasNext()
- 값이 존재하는지 체크한다.
keys.next()
- keys에는 여기에는 값이 2개가 들어있을 것이다. count를 2로 설정했기 때문이다.
ScanCursor.java
를 보면, 실제로moveNext()
에서 source 파라미터를 보면this
가 2개가 들어있다.
즉, count를 2로 설정했기 때문에 하나의 cursorId에 2개의 키가 들어있는 것이다.
여기서 cursor필드는 현재 0인 cursorId에 들어있는 키들의 인덱스인 것 같다. 대략 동작만 파악하면 되기 때문에 더 알아보지는 않았다.
- 여기까지 완료해서 cursorId에서 0번째 key가져온 상태이다. 이후 다시 hasNext()를 하면, cursorId에서 1번째 key를 가져오게 된다.
이렇게 반복되면서 모든 cursorId를 돌면서 corsorId에 있는 corsor들을 가져오는 것이다.
출처
'기타 IT' 카테고리의 다른 글
데드락이란? (0) | 2022.07.05 |
---|---|
RedisTemplate CastClassException(직렬화, 역직렬화 이슈) (0) | 2022.05.12 |
Redis는 언제 사용해야 할까? (0) | 2022.05.11 |
테스트 더블 테스트 VS 실제 객체 테스트(classicist vs mockist) (0) | 2022.04.15 |
SSH란? (0) | 2022.03.22 |