반응형

아직 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을 사용한다.

    • superScanCursor.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들을 가져오는 것이다.

출처


Redis Keys 명령어의 대체 Scan 설명

[우아한테크세미나]191121 우아한레디스 by 강대명님
```

반응형
복사했습니다!