-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Someday TODO언젠간 해야 할 TODO언젠간 해야 할 TODO
Description
아래와 같이 레디스 FallBack 설정 추후에 해야 합니다
@Service
@Slf4j
@RequiredArgsConstructor
public class ReadingDiaryStatisticService {
private static final String DIARY_KEY_PREFIX = "diaries:";
private static final String DIRTY_DIARIES_KEY = "diaries:dirty";
private final StringRedisTemplate redisTemplate;
private final ReadingDiaryStatisticsRepository statisticsRepository;
public void increaseViewCount(Long diaryId, Long memberId) {
String userViewLogKey = DIARY_KEY_PREFIX + diaryId + ":views:log";
Long addResult = redisTemplate.opsForSet().add(userViewLogKey, String.valueOf(memberId));
redisTemplate.expire(userViewLogKey, 1, java.util.concurrent.TimeUnit.DAYS);
boolean isFirstViewIn24Hours = addResult != null && addResult > 0;
if (isFirstViewIn24Hours) {
incrementCount(diaryId, CountType.VIEW);
}
}
public void incrementCount(Long diaryId, CountType type) {
String counterKey = DIARY_KEY_PREFIX + diaryId + ":" + type.name().toLowerCase();
redisTemplate.opsForValue().increment(counterKey);
redisTemplate.opsForSet().add(DIRTY_DIARIES_KEY, String.valueOf(diaryId));
}
public void decrementCount(Long diaryId, CountType type, long amount) {
String counterKey = DIARY_KEY_PREFIX + diaryId + ":" + type.name().toLowerCase();
redisTemplate.opsForValue().decrement(counterKey, amount);
redisTemplate.opsForSet().add(DIRTY_DIARIES_KEY, String.valueOf(diaryId));
}
public Map<Long, Map<CountType, Long>> getCounts(List<Long> diaryIds) {
if (diaryIds == null || diaryIds.isEmpty()) {
return Collections.emptyMap();
}
// 1. Redis에서 데이터 조회
List<String> keys = new ArrayList<>();
for (Long diaryId : diaryIds) {
for (CountType type : CountType.values()) {
keys.add(DIARY_KEY_PREFIX + diaryId + ":" + type.name().toLowerCase());
}
}
List<String> values = redisTemplate.opsForValue().multiGet(keys);
Map<Long, Map<CountType, Long>> result = new HashMap<>();
List<Long> missingDiaryIds = new ArrayList<>();
// 2. Redis 결과 처리 및 누락된 데이터 식별
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = (values != null && values.get(i) != null) ? values.get(i) : null;
String[] parts = key.split(":");
if (parts.length >= 3) {
Long diaryId = Long.parseLong(parts[1]);
CountType type = CountType.valueOf(parts[2].toUpperCase());
if (value != null) {
// Redis에 데이터가 있는 경우
long count = Long.parseLong(value);
result.computeIfAbsent(diaryId, k -> new EnumMap<>(CountType.class)).put(type, count);
} else {
// Redis에 데이터가 없는 경우, DB fallback 대상으로 추가
if (!missingDiaryIds.contains(diaryId)) {
missingDiaryIds.add(diaryId);
}
}
}
}
// 3. DB Fallback: Redis에 없는 데이터를 DB에서 조회
if (!missingDiaryIds.isEmpty()) {
log.warn("Redis에서 누락된 독서일지 통계 데이터를 DB에서 조회합니다. diaryIds: {}", missingDiaryIds);
List<ReadingDiaryStatistic> dbStatistics = statisticsRepository.findAllByReadingDiaryIdIn(missingDiaryIds);
for (ReadingDiaryStatistic stat : dbStatistics) {
Long diaryId = stat.getReadingDiary().getId();
Map<CountType, Long> counts = result.computeIfAbsent(diaryId, k -> new EnumMap<>(CountType.class));
// DB 값을 Redis에 다시 저장 (캐시 워밍업)
this.syncStatisticToRedis(diaryId, stat);
// 결과에 추가
counts.put(CountType.VIEW, (long) stat.getViewCount());
counts.put(CountType.LIKE, (long) stat.getLikeCount());
counts.put(CountType.COMMENT, (long) stat.getCommentCount());
}
// DB에도 없는 독서일지는 기본값(0) 설정
for (Long diaryId : missingDiaryIds) {
if (!result.containsKey(diaryId)) {
Map<CountType, Long> defaultCounts = new EnumMap<>(CountType.class);
for (CountType type : CountType.values()) {
defaultCounts.put(type, 0L);
}
result.put(diaryId, defaultCounts);
}
}
}
return result;
}
/**
* DB 통계 데이터를 Redis에 동기화 (캐시 워밍업)
*/
private void syncStatisticToRedis(Long diaryId, ReadingDiaryStatistic statistic) {
try {
String viewKey = DIARY_KEY_PREFIX + diaryId + ":view";
String likeKey = DIARY_KEY_PREFIX + diaryId + ":like";
String commentKey = DIARY_KEY_PREFIX + diaryId + ":comment";
redisTemplate.opsForValue().set(viewKey, String.valueOf(statistic.getViewCount()));
redisTemplate.opsForValue().set(likeKey, String.valueOf(statistic.getLikeCount()));
redisTemplate.opsForValue().set(commentKey, String.valueOf(statistic.getCommentCount()));
} catch (Exception e) {
log.error("Redis 캐시 워밍업 중 오류 발생. diaryId: {}", diaryId, e);
}
}
public void deleteCounts(List<Long> diaryIds) {
if (diaryIds == null || diaryIds.isEmpty()) {
return;
}
List<String> keysToDelete = new ArrayList<>();
for (Long diaryId : diaryIds) {
for (CountType type : CountType.values()) {
keysToDelete.add(DIARY_KEY_PREFIX + diaryId + ":" + type.name().toLowerCase());
}
// 24시간 조회 기록 로그 Set도 함께 삭제
keysToDelete.add(DIARY_KEY_PREFIX + diaryId + ":views:log");
}
redisTemplate.delete(keysToDelete);
String[] dirtyIdsToRemove = diaryIds.stream()
.map(String::valueOf)
.toArray(String[]::new);
redisTemplate.opsForSet().remove(DIRTY_DIARIES_KEY, (Object[]) dirtyIdsToRemove);
}
}
Metadata
Metadata
Assignees
Labels
Someday TODO언젠간 해야 할 TODO언젠간 해야 할 TODO