티스토리 뷰
Keys 보단, SCAN을 사용하자
# 모든 key를 조회하는 명령인 keys는 사용하지 말고, scan 명령으로 대체하여 사용해야합니다.
KEYS 명령어의 위험성
가장 먼저 구현한 기능은 특정 검색 키워드의 캐시를 삭제하는 기능이었는데, key를 삭제하기 위해서는 먼저 조회를 해야 합니다.
이때 일반적으로 생각할 만한 명령어가 바로 KEYS 입니다. glob pattern으로 간단히 데이터베이스의 모든 key를 조회할 수 있고, 시간 복잡도가 O(N)이기는 하지만 공식 문서에 따르면 저사양 랩탑에서도 40ms 내에 100만 개의 key가 존재하는 데이터베이스를 스캔할 수 있다고 합니다.
그러나 이 명령에는 치명적인 문제점이 있는데, 해당 명령이 실행되는 도중에는 다른 모든 명령의 실행이 블로킹된다는 점입니다.
Redis는 싱글 스레드 아키텍처이기 때문으로, 데이터베이스의 규모가 클 수록 블로킹의 영향으로 성능이 저하되고 장애가 발생할 가능성이 매우 커질 수 있어, 일반적으로 프로덕션 환경에서는 절대 사용하지 말아야 한다고 알려져 있습니다.
그래서 대안으로 SCAN이라는 명령이 존재합니다. KEYS와 달리 다른 명령의 실행을 거의 차단하지 않아 프로덕션에서도 비교적 안전하게 사용할 수 있습니다.
이것이 가능한 원리는, 작은 단위로 증분 반복 순회하면서 데이터베이스를 스캔하기 때문입니다. 명령 호출당 적은 수의 요소만 반환하기 때문에, 중간중간 다른 명령어를 처리할 수 있게 되는 식입니다.
실제 개발하는 소스에서 변경 사항
AS-IS
public List<String> getKeysByPatternOlderThanFiveMinutes(String pattern) {
Set<String> keysSet = stringRedisTemplate.keys(pattern);
log.info("pattern : " + pattern + " keysSet: " + keysSet.toString());
if (keysSet != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);
List<String> filteredKeys = keysSet.stream()
.filter(key -> {
String[] parts = key.split(":");
String[] subParts = parts[1].split("-");
String timestampStr = subParts[0];
try {
LocalDateTime keyTime = LocalDateTime.parse(timestampStr, formatter);
return keyTime.isBefore(fiveMinutesAgo);
} catch (DateTimeParseException e) {
return false;
}
})
.sorted()
.collect(Collectors.toList());
log.info("Keys after sorting: " + filteredKeys);
return filteredKeys;
}
return null;
}
TO-BE
public List<String> getKeysByPatternOlderThanFiveMinutes(String pattern) {
Cursor<byte[]> cursor = stringRedisTemplate.executeWithStickyConnection(redisConnection ->
redisConnection.scan(ScanOptions.scanOptions().match(pattern).count(100).build()));
List<String> keys = new ArrayList<>();
while (cursor.hasNext()) {
keys.add(new String(cursor.next()));
}
log.info("pattern : " + pattern + " keys: " + keys);
if (!keys.isEmpty()) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);
List<String> filteredKeys = keys.stream()
.filter(key -> {
String[] parts = key.split(":");
String[] subParts = parts[1].split("-");
String timestampStr = subParts[0];
try {
LocalDateTime keyTime = LocalDateTime.parse(timestampStr, formatter);
return keyTime.isBefore(fiveMinutesAgo);
} catch (DateTimeParseException e) {
return false;
}
})
.sorted()
.collect(Collectors.toList());
log.info("Filtered and sorted keys: " + filteredKeys);
return filteredKeys;
}
return Collections.emptyList();
}
'Data' 카테고리의 다른 글
dbeaver 쓸것들 (0) | 2023.09.11 |
---|---|
[SQLD/SQLP] 분산 데이터베이스 (0) | 2023.03.09 |
[SQLD/SQLP] 반정규화(De-Normalization) (0) | 2023.03.08 |
[SQLD/SQLP] 데이터 모델링(Data Modeling) (0) | 2023.03.08 |
[SQLD/SQLP] 속성(Attribute) / 관계(Relationship) (0) | 2023.03.08 |