Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 보호 동물 목록 조회 캐시를 리팩토링한다. #458

Merged
merged 4 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface AnimalCacheRepository {

void saveAnimal(Animal animal);

void deleteAnimal(Animal animal);
long deleteAnimal(Animal animal);

FindAnimalsResponse findAnimals(int size, long count);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,89 @@
import com.clova.anifriends.domain.animal.dto.response.FindAnimalsResponse.FindAnimalResponse;
import com.clova.anifriends.domain.animal.repository.response.FindAnimalsResult;
import com.clova.anifriends.domain.common.PageInfo;
import jakarta.annotation.PostConstruct;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class AnimalRedisRepository implements AnimalCacheRepository {

private static final String ANIMAL_ZSET_KEY = "animal";
private static final String TOTAL_NUMBER_OF_ANIMALS_KEY = "total_number_of_animals";
private static final String ANIMAL_ZSET_KEY = "animal:animals";
private static final String TOTAL_NUMBER_OF_ANIMALS_KEY = "animal:total_number";
private static final int ANIMAL_CACHE_SIZE = 30;
private static final long LAST_INDEX = 1L;
private static final long LAST_INDEX = -1;
public static final double NANO = 1_000_000_000.0;

private final RedisTemplate<String, Object> redisTemplate;
private final AnimalRepository animalRepository;

private ZSetOperations<String, Object> zSetOperations;
private final ZSetOperations<String, Object> zSetOperations;
private final ValueOperations<String, Object> valueOperations;

@PostConstruct
public void init() {
zSetOperations = redisTemplate.opsForZSet();

public AnimalRedisRepository(
RedisTemplate<String, Object> redisTemplate,
AnimalRepository animalRepository
) {
this.animalRepository = animalRepository;
this.zSetOperations = redisTemplate.opsForZSet();
this.valueOperations = redisTemplate.opsForValue();
}

public void synchronizeCache() {
zSetOperations.removeRange(ANIMAL_ZSET_KEY, 0, -1);
Pageable pageable = PageRequest.of(0, ANIMAL_CACHE_SIZE);
Slice<FindAnimalsResult> animals = animalRepository.findAnimalsV2(null, null, null, null,
null, null, null, null, pageable);
zSetOperations.removeRange(ANIMAL_ZSET_KEY, 0, LAST_INDEX);

Slice<FindAnimalsResult> animals = getFindAnimalsResults();
animals.forEach(this::saveAnimal);

long dbCount = animalRepository.countAllAnimalsExceptAdopted();
redisTemplate.opsForValue().set(TOTAL_NUMBER_OF_ANIMALS_KEY, dbCount);
valueOperations.set(TOTAL_NUMBER_OF_ANIMALS_KEY, dbCount);
}

@Override
public Long getTotalNumberOfAnimals() {
Object cachedCount = redisTemplate.opsForValue().get(TOTAL_NUMBER_OF_ANIMALS_KEY);
Object cachedCount = valueOperations.get(TOTAL_NUMBER_OF_ANIMALS_KEY);
if (Objects.nonNull(cachedCount)) {
return ((Integer) cachedCount).longValue();
}
long dbCount = animalRepository.countAllAnimalsExceptAdopted();
redisTemplate.opsForValue().set(TOTAL_NUMBER_OF_ANIMALS_KEY, dbCount);
valueOperations.set(TOTAL_NUMBER_OF_ANIMALS_KEY, dbCount);
return dbCount;
}

@Override
public FindAnimalsResponse findAnimals(int size, long count) {
Set<Object> cachedResponses = requireNonNull(
zSetOperations.range(ANIMAL_ZSET_KEY, 0, size - 1L));
PageInfo pageInfo = PageInfo.of(count, count > size);

if (cachedResponses.size() == size) {
List<FindAnimalResponse> responses = requireNonNull(cachedResponses).stream()
.map(FindAnimalResponse.class::cast)
.toList();
return new FindAnimalsResponse(pageInfo, responses);
}

zSetOperations.removeRange(ANIMAL_ZSET_KEY, 0, LAST_INDEX);

Slice<FindAnimalsResult> animalsResult = getFindAnimalsResults();
animalsResult.forEach(this::saveAnimal);

List<FindAnimalResponse> responses = animalsResult.stream()
.map(FindAnimalResponse::from)
.toList();
return new FindAnimalsResponse(pageInfo, responses);
}

@Override
public void saveAnimal(FindAnimalsResult animal) {
FindAnimalResponse findAnimalResponse = FindAnimalResponse.from(animal);
Expand All @@ -79,34 +106,20 @@ public void saveAnimal(Animal animal) {
}

@Override
public void deleteAnimal(Animal animal) {
public long deleteAnimal(Animal animal) {
FindAnimalResponse findAnimalResponse = FindAnimalResponse.from(animal);
zSetOperations.remove(ANIMAL_ZSET_KEY, findAnimalResponse);
}

@Override
public FindAnimalsResponse findAnimals(int size, long count) {
if (requiresCacheUpdate(size)) {
synchronizeCache();
}
Set<Object> cachedResponses = zSetOperations.range(ANIMAL_ZSET_KEY, 0,
size - LAST_INDEX);
List<FindAnimalResponse> responses = requireNonNull(cachedResponses).stream()
.map(FindAnimalResponse.class::cast)
.toList();
PageInfo pageInfo = PageInfo.of(count, count > size);
return new FindAnimalsResponse(pageInfo, responses);
Long number = zSetOperations.remove(ANIMAL_ZSET_KEY, findAnimalResponse);
return isNull(number) ? 0 : number;
}

private void trimCache() {
if (zSetOperations.size(ANIMAL_ZSET_KEY) > ANIMAL_CACHE_SIZE) {
zSetOperations.removeRange(ANIMAL_ZSET_KEY, LAST_INDEX, LAST_INDEX);
}
zSetOperations.removeRange(ANIMAL_ZSET_KEY, ANIMAL_CACHE_SIZE, LAST_INDEX);
}

private boolean requiresCacheUpdate(int size) {
Long count = zSetOperations.size(ANIMAL_ZSET_KEY);
return isNull(count) || count < size;
private Slice<FindAnimalsResult> getFindAnimalsResults() {
Pageable pageable = PageRequest.of(0, ANIMAL_CACHE_SIZE);
return animalRepository.findAnimalsV2(null, null, null,
null, null, null, null, null, pageable);
}

private double getScore(LocalDateTime createdAt) {
Expand All @@ -116,13 +129,11 @@ private double getScore(LocalDateTime createdAt) {

@Override
public void increaseTotalNumberOfAnimals() {
Long cachedCount = getTotalNumberOfAnimals();
redisTemplate.opsForValue().set(TOTAL_NUMBER_OF_ANIMALS_KEY, cachedCount + 1);
valueOperations.increment(TOTAL_NUMBER_OF_ANIMALS_KEY);
}

@Override
public void decreaseTotalNumberOfAnimals() {
Long cachedCount = getTotalNumberOfAnimals();
redisTemplate.opsForValue().set(TOTAL_NUMBER_OF_ANIMALS_KEY, cachedCount - 1);
valueOperations.decrement(TOTAL_NUMBER_OF_ANIMALS_KEY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ public FindAnimalsResponse findAnimalsV2(
Long animalId,
@PageableDefault() Pageable pageable
) {

if (isFirstPage(type, active, neuteredFilter, age, gender, size, createdAt, animalId)) {
return animalCacheRepository.findAnimals(pageable.getPageSize(),
animalCacheRepository.getTotalNumberOfAnimals());
Expand Down Expand Up @@ -172,8 +171,7 @@ public void updateAnimalAdoptStatus(Long shelterId, Long animalId, Boolean isAdo
Animal animal = getAnimalByAnimalIdAndShelterId(animalId, shelterId);
animal.updateAdoptStatus(isAdopted);
if (isAdopted == true) {
animalCacheRepository.deleteAnimal(animal);
animalCacheRepository.decreaseTotalNumberOfAnimals();
deleteFromCache(animal);
}
}

Expand All @@ -192,15 +190,18 @@ public void updateAnimal(
String information,
List<String> imageUrls
) {
Animal foundAnimal = getAnimalByAnimalIdAndShelterIdWithImages(animalId, shelterId);
animalCacheRepository.deleteAnimal(foundAnimal);
Animal animal = getAnimalByAnimalIdAndShelterIdWithImages(animalId, shelterId);
long number = animalCacheRepository.deleteAnimal(animal);

List<String> imagesToDelete = foundAnimal.findImagesToDelete(imageUrls);
List<String> imagesToDelete = animal.findImagesToDelete(imageUrls);
applicationEventPublisher.publishEvent(new ImageDeletionEvent(imagesToDelete));

foundAnimal.updateAnimal(name, birthDate, type, breed, gender, isNeutered, active, weight,
animal.updateAnimal(name, birthDate, type, breed, gender, isNeutered, active, weight,
information, imageUrls);
animalCacheRepository.saveAnimal(foundAnimal);

if (number > 0) {
animalCacheRepository.saveAnimal(animal);
}
}

@Transactional
Expand All @@ -209,6 +210,10 @@ public void deleteAnimal(Long shelterId, Long animalId) {
List<String> imagesToDelete = animal.getImages();
applicationEventPublisher.publishEvent(new ImageDeletionEvent(imagesToDelete));
animalRepository.delete(animal);
deleteFromCache(animal);
}

private void deleteFromCache(Animal animal) {
animalCacheRepository.deleteAnimal(animal);
animalCacheRepository.decreaseTotalNumberOfAnimals();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.clova.anifriends.base;

import com.clova.anifriends.domain.animal.repository.AnimalRedisRepository;
import com.clova.anifriends.domain.animal.repository.AnimalRepository;
import com.clova.anifriends.domain.applicant.repository.ApplicantRepository;
import com.clova.anifriends.domain.chat.repository.ChatMessageRepository;
Expand All @@ -21,6 +22,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;

Expand Down Expand Up @@ -62,6 +64,9 @@ public abstract class BaseIntegrationTest extends TestContainerStarter {
@Autowired
protected AnimalRepository animalRepository;

@Autowired
protected AnimalRedisRepository animalRedisRepository;

@Autowired
protected DonationRepository donationRepository;

Expand All @@ -71,6 +76,9 @@ public abstract class BaseIntegrationTest extends TestContainerStarter {
@Autowired
protected PaymentClient paymentClient;

@Autowired
protected RedisTemplate<String, Object> redisTemplate;

@MockBean
protected ApiService apiService;

Expand Down
Loading
Loading