코드 저장소.

[Coffies Vol.02] Redis keys 대신 Scan을 사용 본문

포폴/Coffies Vol.02

[Coffies Vol.02] Redis keys 대신 Scan을 사용

slown 2024. 7. 29. 23:54

목차

1.Redis keys -> Scan

2.코드 적용

 

1.Redis keys -> Scan

Spring Data Redis에서 KEYS 명령어는 Redis에 있는 전체 키 목록을 가져옵니다. 하지만 KEYS는 몇가지의 문제점을 가지고 있습니다. 우선 KEYS의 문제점은 다음과 같습니다. 

  • 속도 저하 문제
    • KEYS는 Redis에 있는 모든 key를 찾는 방식입니다. 키의 갯수가 많아지면 속도 저하에 문제가 생길 수 있습니다.
  • 블로킹
    • KEYS의 경우에는 실행을 하는 동안에는 다른 요청을 할 수가 없기 때문에 성능에도 문제가 생길 수 있습니다.

그럼 이러한 문제점을 해결을 하기 위해서 찾아본 것이 Redis에 SCAN이라는 명령어가 있어서 사용을 하게 되었습니다.

 

Scan의 설명과 특징은 다음과 같습니다.

  • 설명:
    • Scan명령어는 Keys처럼 한번에 모든 레디스 키를 읽어오는 것이아니라 count 값을 정하여 그 count값만큼 여러번 레디스의 모든 키를 읽어오는 것입니다. 
    • 여기서 count의 개수를 낮게잡으면 count만큼 키를 읽어오는 시간은 적게걸리고 모든 데이터를 읽어오는데 
      시간이 오래걸리지만 그 사이사이 시간에 다른 요청들을 레디스에서 처리합니다.
    • 그리고 count의 개수를 높게 잡으면 count의 개수만큼 읽어오는데 시간이 오래걸리고 모든데이터를 읽는데는 
      시간이 짧게 걸리지만 그 사이사이에 다른 요청을 받는 횟수가 줄어들어 레디스가 다른 요청을 처리하는데 
      병목이 생길 수 있습니다.
  • 장점:
    • 비블로킹: SCAN 명령어는 한 번에 데이터베이스의 작은 부분만을 스캔하므로, 다른 클라이언트의 요청을 처리할 수 있습니다.
    • 점진적 스캔: 이 명령어는 일부분만 스캔하고 커서를 반환하여 다음 호출 시 이어서 검색할 수 있습니다. 이를 통해 큰 데이터베이스에서도 효율적으로 키를 검색할 수 있습니다.
    • 메모리 사용 감소: 한 번에 반환하는 키의 개수를 제한할 수 있어 메모리 사용량을 관리할 수 있습니다.

2.코드 적용

이제 해당 개념을 가지고 현재 적용되고 있는 프로젝트에 적용을 해보겠습니다. 적용할 부분은 어드민 페이지에 회원 아이디 자동완성부분입니다. 

 

    /**
     *  회원 이름 자동완성기능
     * @author 양경빈
     * @param userId 회원 아이디
     * @return searchList 회원 검색에 필요한 목록들
     **/
    public List<String> memberAutoSearch(String userId){

        HashOperations<String,String,Object>hashOperations = redisTemplates.opsForHash();

        List<Member>nameList = memberRepository.findAll();

        log.info(nameList.stream().toList());

        Map<String,Object> nameDateMap = nameList
                .stream()
                .collect(Collectors
                        .toMap(Member::getUserId,Member::getId));

        log.info(nameDateMap);

        //redisHash 에 저장
        hashOperations.putAll(CacheKey.USERNAME,nameDateMap);

        //검색조건 설정
        String matchPattern = "\"" + userId + "*";
        ScanOptions scanOptions = ScanOptions
                .scanOptions()
                .match(matchPattern)
                .count(10000)
                .build();
        
		Cursor<Map.Entry<String,Object>> cursor = hashOperations
                .scan(CacheKey.USERNAME, scanOptions);

        List<String> searchList = new ArrayList<>();
		
        //검색된 키들을 리스트에 추가
        while(cursor.hasNext()){
            Map.Entry<String,Object> entry = cursor.next();
            log.info(entry);
            searchList.add(entry.getKey());
        }

        log.info(searchList);
        return searchList;
    }

 

위의 코드를 설명을 하자면 다음과 같습니다. 

 

1.먼저 데이터 베이스에서 회원의 목록을 전부 가지고 옵니다.

 

2.회원 리스트를 map으로 전환을 해서 redis에 저장을 합니다.

 

3.검색조건을 설정을 합니다.

 

4.Redis에서 패턴에 맞는 키를 검색

 

5.검색된 키를 리스트에 나열해서 보여준다.

 

그럼 위의 작성한 코드가 keys와 얼마나 차이가 나는지를 테스트 코드를 작성을 해서 속도를 측정을 해기로 했습니다. 테스트 코드는 다음과 같습니다. 테스트 시나리오는 더미 데이터로 10000개의 데이터를 redis에 넣어서 key와 scan의 방법으로 측정을 해보는 것입니다.

@SpringBootTest
@AutoConfigureMockMvc
public class MemberRedisTest {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private RedisService redisService;
    
    private HashOperations<String, String, Object> hashOperations;

    @BeforeEach
    public void setup(){
        // Redis 클리어
        redisTemplate.getConnectionFactory().getConnection().flushAll();
        redisTemplate.delete(redisTemplate.keys(CacheKey.USERNAME + "*"));  // Redis 초기화

        // 테스트 데이터 설정
        for (int i = 0; i < 10000; i++) {
            String userId = "user" + i;
            String memberId = "member" + i;
            hashOperations.put(CacheKey.USERNAME, userId, memberId);
        }

    }

    @AfterEach
    public void tearDown() {
        redisTemplate.getConnectionFactory().getConnection().flushAll();
    }

    @Test
    @Disabled
    @DisplayName("회원 자동완성 검색")
    public void memberAutoCompleteTest(){

        HashOperations<String, String, Integer> hashOperations = redisTemplate.opsForHash();
        String key = "USERNAME_AUTOCOMPLETE::";
        hashOperations.put(key,"well4149",0);
        hashOperations.put(key,"well123",0);
        hashOperations.put(key,"well",0);

        //키값으로 저장된 값을 가져오기.=>총3개가 들어왔는지 보기.
        Set<String> a = hashOperations.keys(key);
        System.out.println(a);
        //redis scan을 사용해서 well로 시작하는 단어를 전부 다 검색하기.
        ScanOptions scanOptions = ScanOptions.scanOptions().match("well*").build();
        Cursor<Map.Entry<String,Integer>> cursor= hashOperations.scan(key, scanOptions);

        List<String> searchList = new ArrayList<>();

        while(cursor.hasNext()){
            Map.Entry<String,Integer> entry = cursor.next();
            searchList.add(entry.getKey());
        }

        Assertions.assertThat(searchList).isNotNull();
    }
    
    @Test
    @DisplayName("회원 아이디 자동완성 keys 테스트")
    public void testMemberAutoSearchKeys(){
        // Measure performance of the KEYS-based implementation
        long startTime = System.nanoTime();
        List<String> searchList = redisService.memberAutoSearchKeys("user5060");
        long endTime = System.nanoTime();

        System.out.println("KEYS-based search time: " + (endTime - startTime) + " ns");
        System.out.println("Number of keys found: " + searchList.size());
    }

    @Test
    @DisplayName("회원 아이디 자동완성 scan 테스트")
    public void testMemberAutoSearchScan(){
        // Measure performance of the SCAN-based implementation
        long startTime = System.nanoTime();
        List<String> searchList = redisService.memberAutoSearch("user5060");
        long endTime = System.nanoTime();

        System.out.println("SCAN-based search time: " + (endTime - startTime) + " ns");
        System.out.println("Number of keys found: " + searchList.size());
    }
    
}

 

5회 연속으로 테스트를 돌려본 결과 속도는 다음과 같았다.

 

keys로 조회한 경우

1회 2회 3회 4회 5회
1166042200 ns 1049959100  ns 1187960800 ns 1026097900 ns 1261246300 ns

 

scan으로 조회한 경우

1회 2회 3회 4회 5회
723188000 ns 571165400 ns 580107200 ns 501252100 ns  465099800 ns

 

결과적으로 keys로 전체를 순회를 하는 것보다는 scan으로 순회를 하는 것이 훨씬 속도가 빠르다는 것을 알 수 있었다.