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

NABI-351: Card ViewCount Cache #85

Merged
merged 16 commits into from
Dec 1, 2023
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ dependencies {
// redisson
implementation "org.redisson:redisson-spring-boot-starter:3.21.1"

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Google Firebase Control Message
implementation 'com.google.firebase:firebase-admin:9.1.1'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import org.springframework.boot.context.ApplicationPidFileWriter;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;

import javax.annotation.PostConstruct;
import java.util.TimeZone;

@EnableRetry
@EnableJpaAuditing
@EnableScheduling
@SpringBootApplication
public class NabiMarketBeApplication {
@PostConstruct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.prgrms.nabimarketbe.domain.card.service.CardService;
import org.prgrms.nabimarketbe.domain.category.entity.CategoryEnum;
import org.prgrms.nabimarketbe.domain.item.entity.PriceRange;
import org.prgrms.nabimarketbe.global.util.KeyGenerator;
import org.prgrms.nabimarketbe.global.util.OrderCondition;
import org.prgrms.nabimarketbe.global.util.ResponseFactory;
import org.prgrms.nabimarketbe.global.util.model.CommonResult;
Expand Down Expand Up @@ -64,7 +65,10 @@ public ResponseEntity<SingleResult<CardUserResponseDTO>> getCardById(
@RequestHeader(value = "Authorization", required = false) String token,
@PathVariable Long cardId
) {
CardUserResponseDTO cardSingleReadResponseDTO = cardService.getCardById(token, cardId);
CardUserResponseDTO cardSingleReadResponseDTO = cardService.getCardById(
token,
cardId
);

return ResponseEntity.ok(ResponseFactory.getSingleResult(cardSingleReadResponseDTO));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.prgrms.nabimarketbe.domain.card.entity.Card;
import org.prgrms.nabimarketbe.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -17,4 +18,11 @@ public interface CardRepository extends JpaRepository<Card, Long>, CardRepositor

@Query("SELECT c FROM Card c WHERE c.cardId = :cardId")
Optional<Card> findExistingCardById(@Param("cardId") Long cardId);

@Query("SELECT c.viewCount FROM Card c WHERE c.cardId = :cardId")
Integer getCardViewCountById(@Param("cardId") Long cardId);

@Query("UPDATE Card c SET c.viewCount = :viewCount WHERE c.cardId = :cardId")
@Modifying(clearAutomatically = true)
void updateViewCountByCardId(@Param("cardId") Long cardId, @Param("viewCount") Integer viewCount);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ public class CardService {

private final UserRepository userRepository;

private final CheckService checkService;

private final DibRepository dibRepository;

private final CardImageBatchRepository cardImageBatchRepository;

private final CheckService checkService;

private final CardViewCountService cardViewCountService;

@Transactional
public CardResponseDTO<CardCreateResponseDTO> createCard(
String token,
Expand Down Expand Up @@ -156,7 +158,7 @@ public CardResponseDTO<CardUpdateResponseDTO> updateCardById(
}

@Transactional
public CardUserResponseDTO getCardById(
public CardUserResponseDTO getCardById (
String token,
Long cardId
) {
Expand All @@ -171,7 +173,11 @@ public CardUserResponseDTO getCardById(
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND));

if (!checkService.isEqual(userId, card.getUser().getUserId())) {
card.updateViewCount();
cardViewCountService.increaseViewCount(
userId,
cardId
);

isMyDib = dibRepository.existsDibByCardAndUser(card, loginUser);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.prgrms.nabimarketbe.domain.card.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.prgrms.nabimarketbe.domain.card.repository.CardRepository;
import org.prgrms.nabimarketbe.global.redisson.RedisDAO;
import org.prgrms.nabimarketbe.global.util.KeyGenerator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;
import java.util.Set;

@Service
@Slf4j
@RequiredArgsConstructor
public class CardViewCountService {
private final RedisDAO redisDAO;

private final CardRepository cardRepository;

@Async("threadPoolTaskExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void increaseViewCount(
Long userId,
Long cardId
) {
// 조회수 로직 시작
String cardViewCacheKey = KeyGenerator.generateCardViewCacheKey(cardId); // 조회수 key
String readerCacheKey = KeyGenerator.generateCardReaderCacheKey(userId); // 유저 key

// 유저를 key로 조회한 게시글 ID List안에 해당 게시글 ID가 포함되어있지 않는다면,
if (!redisDAO.getValuesList(readerCacheKey).contains(cardViewCacheKey)) {
// TODO: 캐시 데이터 삭제 주기 고려해보기
redisDAO.setValuesList(readerCacheKey, cardViewCacheKey); // 유저 key로 해당 글 ID를 List 형태로 저장

redisDAO.increment(cardViewCacheKey); // 조회수 증가
}
}

@Transactional
@Scheduled(cron = "0 0/10 * * * ?")
public void flushCardViewCacheToRDB() {
log.info("schedule start!");
Set<String> cardViewCountKeys = redisDAO.getKeySet(KeyGenerator.CARD_VIEW_CACHE_PREFIX + "*");

if (Objects.requireNonNull(cardViewCountKeys).isEmpty()) {
return;
}

for (String cardViewCountKey : cardViewCountKeys) { // TODO: 추후에 bulk update로 전환
Long cardId = Long.valueOf(cardViewCountKey.split(":")[1]);
Integer cardViewAdd = Integer.parseInt(redisDAO.getValue(cardViewCountKey)); // 조회수 증가값

// DB에 캐시 데이터 반영
Integer cardViewCount = cardRepository.getCardViewCountById(cardId); // 기존 조회수 값
cardViewCount = cardViewCount + cardViewAdd; // 조회수 증가 적용
cardRepository.updateViewCountByCardId(cardId, cardViewCount);

// 캐시 데이터 삭제
redisDAO.deleteValues(cardViewCountKey);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.prgrms.nabimarketbe.domain.suggestion.entity.DirectionType;
import org.prgrms.nabimarketbe.domain.suggestion.entity.SuggestionType;
import org.prgrms.nabimarketbe.domain.suggestion.service.SuggestionService;
import org.prgrms.nabimarketbe.global.redisson.LockKeyGenerator;
import org.prgrms.nabimarketbe.global.util.KeyGenerator;
import org.prgrms.nabimarketbe.global.util.ResponseFactory;
import org.prgrms.nabimarketbe.global.util.model.SingleResult;

Expand All @@ -28,7 +28,7 @@ public ResponseEntity<SingleResult<SuggestionResponseDTO>> createSuggestion(
@PathVariable String suggestionType,
@RequestBody SuggestionRequestDTO suggestionRequestDTO
) {
String key = LockKeyGenerator.generateSuggestionKey(
String key = KeyGenerator.generateSuggestionLockKey(
suggestionRequestDTO.fromCardId(),
suggestionRequestDTO.toCardId()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.prgrms.nabimarketbe.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();

taskExecutor.setCorePoolSize(3);
taskExecutor.setMaxPoolSize(11);
taskExecutor.setQueueCapacity(10);
taskExecutor.setThreadNamePrefix("Executor-");
taskExecutor.initialize();

return taskExecutor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.prgrms.nabimarketbe.global.config;

import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;

@Value("${spring.redis.port}")
private int redisPort;

private static final String REDISSON_HOST_PREFIX = "redis://";

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);

return Redisson.create(config);
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());

return redisTemplate;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.prgrms.nabimarketbe.global.redisson;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class RedisDAO {
private final RedisTemplate<String, String> redisTemplate;

public String getValue(String key) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
return values.get(key);
}

public List<String> getValuesList(String key) {
Long len = redisTemplate.opsForList().size(key);
return len == 0 ? new ArrayList<>() : redisTemplate.opsForList().range(key, 0, len-1);
}

public Set<String> getKeySet(String keyPattern) {
return redisTemplate.keys(keyPattern);
}

public void increment(String key) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.increment(key, 1);
}

public void setValues(String key, String data) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(key, data);
}

public void setValues(String key, String data, Duration duration) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(key, data, duration);
}

public void setValuesList(String key, String data) {
redisTemplate.opsForList().rightPushAll(key, data);
}

public void deleteValues(String key) {
redisTemplate.delete(key);
}
}

This file was deleted.

Loading
Loading