
조회수는 게시글이 조회된 횟수만 저장하면 된다.
데이터 일관성이 덜 중요하고, 다른 데이터에 의해 파생되는 데이터가 아니므로, 트랜잭션이나 안전한 저장소가 반드시 필요하진 않다. 또한 디스크는 접근 비용이 비싸다. 디스크보다 더 빠른 저장소를 고려해 볼 수 있다.
그렇다면 조회수 관리를 위해 In-memory Database를 사용해볼 수 있다. Redis
Redis는 휘발성이다. 종료되면 저장되어 있던 데이터는 사라진다. 그렇다면 Redis에는 백업이 필요하다.
약간의 데이터 유실을 허용했기 때문에 모든 데이터를 백업할 필요는 없다.
게시글을 조회하면 조회수가 올라간다. 어뷰저는 특정 게시글을 여러 번 조회해서 데이터를 조작할 수 있다. 조회수 기반으로 인기글이 산정되는 경우, 올바르지 않는 어뷰징으로 인기글이 선정될 수 있다.
어뷰징을 방지하기 위한 조회 여부는
로그인 사용자만 고려하고 설계하게 한다.
key는 ? + userId가 된다.| 요청 1 | Redis | 요청 2 |
|---|---|---|
| 조회수=3 | ||
| 조회수 증가 분산 락 요청 key=articleId+userId, TTL=10분 | 조회수=3 lock=(key=articleId+userId, TTL=10분) | |
| setIfAbsent -> True 응답 | 조회수=3 lock=(key=articleId+userId, TTL=10분) | |
| 조회수=3 lock=(key=articleId+userId, TTL=10분) | 조회수 증가 분산 락 요청 key=articleId+userId, TTL=10분 | |
| 조회수=4 lock=(key=articleId+userId, TTL=10분) | setIfAbsent -> False 응답 | |
| 조회수 증가 | 조회수=4 lock=(key=articleId+userId, TTL=10분) | 종료 |
동일 사용자가 2번의 조회수 증가 요청이 있을 때의 상황이다.
@Service
@RequiredArgsConstructor
public class ArticleViewService {
private final ArticleViewCountRepository articleViewCountRepository;
private final ArticleViewCountBackUpProcessor articleViewCountBackUpProcessor;
private final ArticleViewDistributedLockRepository articleViewDistributedLockRepository;
private static final int BACK_UP_BACH_SIZE = 100;
private static final Duration TTL = Duration.ofMinutes(10);
public Long increase(Long articleId, Long userId) {
if (!articleViewDistributedLockRepository.lock(articleId, userId, TTL)) {
return articleViewCountRepository.read(articleId);
}
Long count = articleViewCountRepository.increase(articleId);
if(count % BACK_UP_BACH_SIZE == 0){
articleViewCountBackUpProcessor.backUp(articleId, userId);
}
return count;
}
로그인 사용자 기준으로 조회수를 관리하기로 했고, 조회수 증가에서 articleId, userId로 락의 여부를 확인한다.
.lock(articleId, userId, TTL) 에서 사용자 A가 해당 게시글을 조회한 여부가 있는지 확인한다. 처음 조회한다면 락을 획득하고 아니면 락 획득에 실패해 해당 게시글의 조회수를 반환한다.