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

127 feat 인기 포트폴리오 방식 변경 #128

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
12 changes: 11 additions & 1 deletion src/main/java/com/palettee/global/configs/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.palettee.chat.controller.dto.response.ChatResponse;
import com.palettee.global.redis.sub.RedisSubscriber;
import com.palettee.portfolio.controller.dto.response.PortFolioPopularResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
Expand Down Expand Up @@ -112,6 +113,15 @@ public RedisTemplate<String, ChatResponse> chatRedisTemplate(RedisConnectionFact
return redisTemplate;
}

@Bean
public RedisTemplate<String, PortFolioPopularResponse> portFolioPopular(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, PortFolioPopularResponse> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
Expand All @@ -130,7 +140,7 @@ public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(30));
.entryTtl(Duration.ofDays(7));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/palettee/global/configs/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ public BypassUrlHolder bypassUrlHolder() {

/* <-------------- Portfolio API --------------> */
// 포트폴리오 전체 조회
.byPassable("/portFolio", HttpMethod.GET)
.conditionalByPassable("/portFolio", HttpMethod.GET)

// 메인 인기 포트폴리오 페이지
.byPassable("/portFolio/main", HttpMethod.GET)
.conditionalByPassable("/portFolio/main", HttpMethod.GET)

// 소모임 전체 조회
.byPassable("/gathering", HttpMethod.GET)
Expand Down
107 changes: 91 additions & 16 deletions src/main/java/com/palettee/global/redis/service/RedisService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import com.palettee.likes.controller.dto.LikeDto;
import com.palettee.likes.domain.LikeType;
import com.palettee.likes.service.LikeService;
import com.palettee.portfolio.controller.dto.response.PortFolioPopularResponse;
import com.palettee.portfolio.domain.PortFolio;
import com.palettee.portfolio.repository.PortFolioRepository;
import com.palettee.portfolio.service.PortFolioRedisService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -24,9 +27,12 @@ public class RedisService {

private final RedisTemplate<String, Long> redisTemplate;
private final PortFolioRedisService portFolioRedisService;

private final PortFolioRepository portFolioRepository;
private final MemoryCache memoryCache;
private final LikeService likeService;

private final RedisTemplate<String, PortFolioPopularResponse> responseRedisTemplate;
/**
* redis에 해당 뷰 카운팅
* @param targetId 는 achieveId, portFolioId
Expand All @@ -39,16 +45,18 @@ public boolean viewCount(Long targetId, Long userId ,String category) {
String key = VIEW_PREFIX + category + ": " + targetId;
String userKey = key + "_user";


// 해당 카테고리의 아이디에 대한 유저가 존재하는지 여부 -> 존재하면 해당 목록에 저장 후 카운팅, 존재 x 노 카운팅
Long validation = redisTemplate.opsForSet().add(userKey, userId);

if(validation != null && validation > 0) {
log.info("조회수 카운팅");
redisTemplate.opsForValue().increment(key, 1L);

return true;
}
log.info("24시간이 지나야 조회 할 수 있습니다");
return false;
log.info("24시간이 지나야 조회 할 수 있습니다");
return false;

}

Expand All @@ -66,17 +74,20 @@ public boolean viewCount(Long targetId, Long userId ,String category) {
public boolean likeCount(Long targetId, Long userId, String category) {
String key = LIKE_PREFIX + category + ": " + targetId;
String userKey = key + "_user";
String userTargetsKey = LIKE_PREFIX + category + ": " + userId + "_targets"; // userId가 좋아요를 누른 모든 targetId를 추적

Long validation = redisTemplate.opsForSet().add(userKey, userId);

if (validation != null && validation > 0) {
log.info("좋아요를 눌렀습니다");
redisTemplate.opsForValue().increment(key, 1L);
redisTemplate.opsForSet().add(userTargetsKey, targetId);
return true;
} else {
log.info("좋아요를 취소하였습니다");
redisTemplate.opsForSet().remove(userKey, userId);
redisTemplate.opsForValue().set(key, 0L); //decrement 를 안한 이유: count redis 가 reset 된 후에 좋아요 취소하면 -1 값이 들어감
redisTemplate.opsForSet().remove(userTargetsKey, targetId);
return false;
}
}
Expand Down Expand Up @@ -130,13 +141,17 @@ public void likeRedisToDB(String likeKeys, String category) {
}

redisKeys = redisKeys.stream()
.filter(redisKey -> !redisKey.contains("_user"))
.filter(redisKey -> !redisKey.contains("_user") && !redisKey.contains("_targets"))
.collect(Collectors.toSet());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나도 성능 체크를 해보긴 해야하는데
contains 보다 userTargetKey.split("_")[2].equals("targets");
이런식으로 구현하면 더 좋지 않을까 싶긴 함


redisKeys.forEach(redisKey -> {
log.info("redisKey ={}", redisKey);
long targetId = Long.parseLong(redisKey.replace(likeKeys, "").trim());
insertLikeDB(category, redisKey, targetId); // 배치 insert
Long countLikes = Optional.ofNullable(redisTemplate.opsForValue().get(redisKey)).orElse(0L); // view count 인 value 를 가져옴
if(countLikes > 0) {
// 배치 insert
insertLikeDB(category, redisKey, targetId);
}
});
}

Expand All @@ -145,7 +160,14 @@ public void likeRedisToDB(String likeKeys, String category) {
*/
public void rankingCategory(String category) {
String zSetKey = category + "_Ranking";
redisTemplate.opsForZSet().removeRange(zSetKey, 0, -1); // 인기 순위 목록 한번 비우기

log.info("memort CacheSize ={}", memoryCache.getLocalCache().size());

//memory cache 있을시에 한번 비우기
if(memoryCache.getLocalCache() != null && !memoryCache.getLocalCache().isEmpty()){
responseRedisTemplate.opsForZSet().removeRange(zSetKey, 0, -1);
}


Map<String, Long> viewCache = memoryCache.getViewCache(category);
Map<String, Long> likeCache = memoryCache.getLikeCache(category);
Expand All @@ -156,23 +178,55 @@ public void rankingCategory(String category) {
String viewsKeys = VIEW_PREFIX + category + ": ";
String likesKeys = LIKE_PREFIX + category + ": ";

//가중치 가져와서 합산 후에 더 한 값을 Zset에 반영
// 아이디를 통해 db에서 한번에 조회하기 위해서
Set<Long> setList = new HashSet<>();

//set에 가중치에 key에 대한 아이디들 저장
viewCache.forEach((keys, value) -> {
log.info("viewKeys ={}", keys);
Long targetId = Long.parseLong(keys.replace(viewsKeys, "").trim());
Double score = redisTemplate.opsForZSet().score(zSetKey, targetId);
double resultScore = (score == null ? 0 : score) + value;
redisTemplate.opsForZSet().add(zSetKey, targetId, resultScore);
setList.add(targetId);
});

//가중치 가져와서 합산 후에 더 한 값을 Zset에 반영
//set에 가중치에 key에 대한 아이디들 저장
likeCache.forEach((keys, value) -> {
log.info("keys ={}", keys);
long targetLikeId = Long.parseLong(keys.replace(likesKeys, "").trim());
Double score = redisTemplate.opsForZSet().score(zSetKey, targetLikeId);
double resultScore = (score == null ? 0 : score) + value;
redisTemplate.opsForZSet().add(zSetKey, targetLikeId, resultScore);
Long targetId = Long.parseLong(keys.replace(likesKeys, "").trim());
setList.add(targetId);
});

if(!setList.isEmpty()) {
//아이디들을 통해 포트폴리오들을 가져옴
List<PortFolio> portFolios = portFolioRepository.findAllByPortfolioIdIn(new ArrayList<>(setList));

// 해당 아이디에 대한 포트폴리오 매핑
Map<Long, PortFolio> collect = portFolios.stream()
.collect(Collectors.toMap(PortFolio::getPortfolioId, portFolio -> portFolio));

//가중치
viewCache.forEach((keys, value) -> {
Long targetId = Long.parseLong(keys.replace(viewsKeys, "").trim());
PortFolio portFolio = collect.get(targetId);
if(portFolio != null){
Double score = responseRedisTemplate.opsForZSet().score(zSetKey, PortFolioPopularResponse.toDto(portFolio));
log.info("viewScore={}", score);
double result = ((score == null) ? 0 : score) + value;
responseRedisTemplate.opsForZSet().add(zSetKey, PortFolioPopularResponse.toDto(portFolio), result);
}
});

likeCache.forEach((keys, value) -> {
Long targetId = Long.parseLong(keys.replace(likesKeys, "").trim());
log.info("targetId ={}", targetId);
PortFolio portFolio = collect.get(targetId);
if(portFolio != null) {
log.info("값이 잇음");
Double score = responseRedisTemplate.opsForZSet().score(zSetKey, PortFolioPopularResponse.toDto(portFolio));
log.info("LikeScore={}", score);
double result = ((score == null) ? 0 : score) + value;
responseRedisTemplate.opsForZSet().add(zSetKey, PortFolioPopularResponse.toDto(portFolio), result);
}
});

}
}

/**
Expand Down Expand Up @@ -235,6 +289,23 @@ public void deleteKeyExceptionPattern(String pattern, String userKeySuffix) {
}
}
}
public void deleteKeyExceptionPatterns(String pattern, String userKeySuffix, String targetKeySuffix) {
Set<String> keys = redisTemplate.keys(pattern);

if (keys != null && !keys.isEmpty()) {
Set<String> keyDelete;
if (userKeySuffix == null) {
keyDelete = keys;
} else {
keyDelete = keys.stream()
.filter(key -> !key.contains(userKeySuffix) && !key.contains(targetKeySuffix))
.collect(Collectors.toSet());
}
if (!keyDelete.isEmpty()) {
redisTemplate.delete(keyDelete);
}
}
}

/**
*
Expand Down Expand Up @@ -315,6 +386,10 @@ public Long likeCountInRedis(String category, Long targetId){
return null;
}

public Set<Long> getLikeTargetIds(Long userId, String category){
return redisTemplate.opsForSet().members( LIKE_PREFIX + category + ": " + userId + "_targets");
}


/**
*
Expand Down
13 changes: 7 additions & 6 deletions src/main/java/com/palettee/global/scheduler/RedisScheduled.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ public void updateRedisToDb(){
redisService.categoryToDb("portFolio");
redisService.categoryToDb("gathering");

//여기에 아카이빙이나 게더링 넣으시면 됩니다
// //여기에 아카이빙이나 게더링 넣으시면 됩니다
// redisService.deleteKeyIncludePattern("Like_*", "_targetCount");
}

/**
* 한식간 마다 로컬 캐시에 있는 가중치를 꺼내어 Zset에 반영
* 매 10분마다 가중치 반영
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적으로 시간 단위가 통일되어 있지 않은 느낌이 있음.
이전 PR에서도 어떤건 6시간 3시간 적혀있고, 12시간 마다 동작하는거도 있고 등등 한번 정리가 필요할듯

*/
@Scheduled(cron = "3 * * * * *")
public void rankingRedis(){
Expand All @@ -37,7 +38,7 @@ public void rankingRedis(){
redisRankingZset();

//순위 여부 확인 후 비우기
redisCacheDelete();
// redisCacheDelete();

// 여기에 아카이브 넣으시면 됩니다.

Expand All @@ -48,13 +49,13 @@ public void rankingRedis(){

//카운트 redis 한번 비우기
redisService.deleteKeyExceptionPattern("View_*", "_user");
redisService.deleteKeyExceptionPattern("Like_*", "_user");
redisService.deleteKeyExceptionPatterns("Like_*", "_user","_targets");
}

/**
* 자정 시간 조회수 리셋 즉 하루에 한번은 카테고리를 조회 할 수 있음
*/
@Scheduled(cron = "4 * * * * *", zone = "Asia/Seoul")
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void hitsSet(){
redisService.deleteKeyIncludePattern("View_*", "_user");
}
Expand All @@ -63,7 +64,7 @@ public void hitsSet(){
* ZSET 한번 비우고 새로운 가중치 ZSET 반영
*/
private void redisRankingZset() {
redisService.deleteKeyExceptionPattern("portFolio_*", null);
// redisService.deleteKeyExceptionPattern("portFolio_*", null);

redisService.rankingCategory("portFolio");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.palettee.likes.domain.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

Expand All @@ -29,8 +31,12 @@ public interface LikeRepository extends JpaRepository<Likes, Long> {
@Query("select count(l) from Likes l where l.targetId = :targetId and l.likeType = 'GATHERING'")
long countByTargetId(Long targetId);

@Query("SELECT l.targetId FROM Likes l WHERE l.targetId IN :targetIds and l.likeType = 'PORTFOLIO' and l.user.id = :userId")
Set<Long> findByTargetIdAndPortFolio(Long userId, List<Long> targetIds);


@Modifying
@Query("delete from Likes l where l.user.id = :userId and l.targetId = :targetId and l.likeType = :likeType")
void deleteAllByTargetId(Long userId, Long targetId, LikeType likeType);


@Modifying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import com.palettee.portfolio.controller.dto.response.PortFolioResponse;
import com.palettee.portfolio.controller.dto.response.PortFolioWrapper;
import com.palettee.portfolio.service.PortFolioService;
import com.palettee.user.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
@RequiredArgsConstructor
@RequestMapping("/portFolio")
Expand All @@ -28,7 +31,7 @@ public Slice<PortFolioResponse> findAll(
@RequestParam(required = false) String majorJobGroup,
@RequestParam(required = false) String minorJobGroup
){
return portFolioService.findAllPortFolio(pageable,majorJobGroup, minorJobGroup,sort);
return portFolioService.findAllPortFolio(pageable,majorJobGroup, minorJobGroup,sort, getUserFromContext());
}

@GetMapping("/{portFolioId}")
Expand All @@ -55,13 +58,21 @@ public boolean createLikes(
@GetMapping("/main")
public PortFolioWrapper findPopular(){

return portFolioService.popularPortFolio();
return portFolioService.popularPortFolio(getUserFromContext());
}

private Optional<User> getUserFromContext() {
User user = null;
try {
user = UserUtils.getContextUser();
} catch (Exception e) {
log.info("Current user is not logged in");
}

return Optional.ofNullable(user);
}

// private static boolean firstPage(Pageable pageable) {
// boolean isFirst = pageable.getPageNumber() == 0;
// return isFirst;
// }


private static boolean isLikedFirst(Long likeId) {
if(likeId == null){
Expand Down
Loading
Loading