Skip to content
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 @@ -10,6 +10,7 @@
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

Expand All @@ -20,15 +21,15 @@ public class ConcertRedisRepository {
private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, Object> objectRedisTemplate;

private static final String LOCK_FLAG_PREFIX = "initLoad: ";
private static final String LOCK_FLAG_PREFIX = "initLoad:";

private static final String CONCERT_DETAIL_PREFIX = "concertDetail: ";
private static final String CONCERT_DETAIL_PREFIX = "concertDetail:";

private static final String CONCERT_LIST_PREFIX = "concertList: ";
private static final String CONCERT_LIST_PREFIX = "concertList:";

private static final String VIEW_COUNT_MAP = "viewCountMap";
private static final String CONCERTS_COUNT_PREFIX = "totalConcertsCount:";

private static final String CONCERTS_COUNT_PREFIX = "totalConcertsCount: ";
private static final String CONCERTS_VIEW_COUNTS = "concertsViewCount";

private static final int HOUR = 3600;

Expand All @@ -50,7 +51,7 @@ public void unlockSave(String key) {
}

// 공연 목록 캐싱
public void listSave(ListSort sort, Pageable pageable, List<ConcertItem> list) {
public void saveConcertsList(ListSort sort, Pageable pageable, List<ConcertItem> list) {
String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber();
objectRedisTemplate.opsForValue().set(key, list, HOUR, TimeUnit.SECONDS);
}
Expand All @@ -70,64 +71,70 @@ public void deleteAllConcertsList() {
}

// 공연 상세 캐싱
public void detailSave(long concertId, ConcertDetailResponse concertDetailResponse) {
public void saveConcertDetail(Long concertId, ConcertDetailResponse concertDetailResponse) {
objectRedisTemplate.opsForValue().set(
CONCERT_DETAIL_PREFIX + concertId,
concertDetailResponse,
HOUR,
TimeUnit.SECONDS
2,
TimeUnit.DAYS
);
redisTemplate.opsForHash().put(CONCERTS_VIEW_COUNTS, concertId.toString(), concertDetailResponse.getViewCount() + "");
}

// 공연 정보 가져오기
public ConcertDetailResponse getCachedConcertDetail(Long concertId) {
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(concertId);
if (concertDetailResponse == null) return null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

null 반환보다 예외처리를 하는게 좋아보입니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

해당 처리 부분을 가져다 쓰는 서비스 로직에서 처리가 됩니다!

return concertDetailResponse;
}

// todo : 객체 일부의 값만 바뀌는거니 해당 값만 바꿔서 저장하거나 Redis 내부의 값만 갱신할 수 있는 방법 찾기
public ConcertDetailResponse getDetail(long concertId) {
private ConcertDetailResponse getConcertDetailResponse(long concertId) {
ConcertDetailResponse concertDetailResponse = (ConcertDetailResponse) objectRedisTemplate.opsForValue().get(CONCERT_DETAIL_PREFIX + concertId);
if (concertDetailResponse == null) return null;
int viewCount = concertDetailResponse.getViewCount();
viewCountSet(concertId, viewCount + 1);
concertDetailResponse.setViewCount(viewCount + 1);
detailSave(concertId, concertDetailResponse);
return concertDetailResponse;
}

// 공연 상세 삭제
public void deleteDetail(String concertId) {
public void deleteConcertDetail(String concertId) {
redisTemplate.delete(CONCERT_DETAIL_PREFIX + concertId);
}

// 모든 공연 상세 삭제
public void deleteAllConcertDetail() {
public void deleteAllCachedConcertDetail() {
deleteAllItemsByPREFIX(CONCERT_DETAIL_PREFIX);
}

// 조회수 처리 -> 좀 지저분한데 개선 여지 찾아보기
public int viewCountSet(long concertId, int viewCount) {
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);

if (rawMap == null) {
Map<Long, Integer> viewCountMap = new HashMap<>();
viewCountMap.put(concertId, viewCount);
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
} else {
Map<Long, Integer> viewCountMap = convertViewCountMap(rawMap);
viewCountMap.put(concertId, viewCount);
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
log.info(viewCountMap.size() + "view count size.");
// 모든 공연의 조회수 맵 조회 -> 하나의 해시를 기준으로 가져올 수 있게 처리
public Map<Long, Integer> getCachedViewCountMap() {
Map<Object, Object> rawMap = redisTemplate.opsForHash().entries(CONCERTS_VIEW_COUNTS);
Map<Long, Integer> viewCountMap = new HashMap<>();
for (Map.Entry<Object, Object> rawEntity : rawMap.entrySet()) {
Long concertID = Long.valueOf(rawEntity.getKey().toString());
Integer viewCount = Integer.valueOf(rawEntity.getValue().toString());
viewCountMap.put(concertID, viewCount);
}
return viewCount;
return viewCountMap;
}

// 조회수 맵 조회
public Map<Long, Integer> getViewCountMap() {
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);
if (rawMap == null) return null;
objectRedisTemplate.delete(VIEW_COUNT_MAP);
return convertViewCountMap(rawMap);
// 공연의 조회수 조회
public Long getCachedViewCount(Long concertId) {
Long viewCount = Long.valueOf(redisTemplate.opsForHash()
.get(
CONCERTS_VIEW_COUNTS,
concertId.toString()
)
.toString());

return viewCount;
}

// 조회수 맵 삭제
public void deleteViewCountMap() {
objectRedisTemplate.delete(VIEW_COUNT_MAP);
// 공연에 예매시작, 종료일자 추가
public void updateCachedTickingDate(Long concertId, LocalDateTime TicketTime, LocalDateTime TicketEndTime) {
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(concertId);
if (concertDetailResponse == null) return;
concertDetailResponse.setTicketTime(TicketTime);
concertDetailResponse.setTicketEndTime(TicketEndTime);
saveConcertDetail(concertId, concertDetailResponse);
}

// String Integer 타입 맵을 Long Integer로 변환
Expand Down Expand Up @@ -164,20 +171,20 @@ private void deleteAllItemsByPREFIX(String prefix) {

// 총 공연의 개수 저장
public Long saveTotalConcertsCount(Long totalConcertsCount, ListSort sort) {
redisTemplate.opsForValue().set(CONCERTS_COUNT_PREFIX + sort.name(),totalConcertsCount.toString());
redisTemplate.opsForValue().set(CONCERTS_COUNT_PREFIX + sort.name(), totalConcertsCount.toString());
return totalConcertsCount;
}

// 총 공연의 개수 조회
public Long getTotalConcertsCount(ListSort sort) {
String raw = redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name());
String raw = redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name());
if (raw == null) return -1L;
else return Long.parseLong(redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name()));
else return Long.parseLong(redisTemplate.opsForValue().get(CONCERTS_COUNT_PREFIX + sort.name()));
}

// 총 공연의 개수 삭제
public void deleteTotalConcertsCount(ListSort sort) {
redisTemplate.delete(CONCERTS_COUNT_PREFIX+sort.name());
redisTemplate.delete(CONCERTS_COUNT_PREFIX + sort.name());
}

// 사용자가 좋아요를 누른 공연의 개수 조회(임시 캐시 느낌으로 짧게 저장, 조회시 시간 갱신 ~1일
Expand All @@ -189,7 +196,7 @@ public Long getUserLikedCount(User user) {
}

// 사용자가 좋아요를 누른 공연의 개수 저장
public Long saveUserLikedCount(User user,Long count) {
public Long saveUserLikedCount(User user, Long count) {
redisTemplate.opsForValue().set(
CONCERTS_COUNT_PREFIX + user.getId(),
count.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public List<ConcertItem> getConcertsList(Pageable pageable, ListSort sort) {
case REGISTERED -> concertItems = concertRepository.getConcertItemsOrderByApiId(pageable);
}

concertRedisRepository.listSave(sort,pageable,concertItems);
concertRedisRepository.saveConcertsList(sort,pageable,concertItems);
return concertItems;
}

Expand Down Expand Up @@ -100,34 +100,33 @@ public void setAutoComplete(){
// 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지
@Transactional
public ConcertDetailResponse getConcertDetail(long concertId) {
ConcertDetailResponse concertDetailResponse = concertRedisRepository.getDetail(concertId);
ConcertDetailResponse concertDetailResponse = concertRedisRepository.getCachedConcertDetail(concertId);
if(concertDetailResponse == null){
concertDetailResponse = concertRepository.getConcertDetailById(concertId);
List<ConcertImage> concertImages = concertImageRepository.getConcertImagesByConcert_ConcertId(concertId);
List<String> concertImageUrls = new ArrayList<>();
for(ConcertImage concertImage : concertImages){
concertImageUrls.add(concertImage.getImageUrl());
}

concertRepository.concertViewCountUp(concertId);
concertDetailResponse.setConcertImageUrls(concertImageUrls);
concertDetailResponse.setViewCount(concertDetailResponse.getViewCount() + 1);
concertRedisRepository.detailSave(concertId, concertDetailResponse);
}
// 조회수 1 증가하고 해당 데이터를 캐시에 저장.
concertDetailResponse.setViewCount(concertDetailResponse.getViewCount() + 1);
concertRedisRepository.saveConcertDetail(concertId, concertDetailResponse);
return concertDetailResponse;
}

// 조회수 갱신
@Transactional
public void viewCountUpdate(){
Map<Long,Integer> viewCountMap = concertRedisRepository.getViewCountMap();
Map<Long,Integer> viewCountMap = concertRedisRepository.getCachedViewCountMap();
if(viewCountMap == null || viewCountMap.isEmpty()) {
log.info("viewCountMap is empty");
} else{
for (Map.Entry<Long, Integer> viewCountEntry : viewCountMap.entrySet()) {
concertRepository.concertViewCountSet(viewCountEntry.getKey(), viewCountEntry.getValue());
}
concertRedisRepository.deleteViewCountMap();
concertRedisRepository.deleteAllConcertsList();
log.info("viewCount updated");
}
Expand Down Expand Up @@ -238,7 +237,10 @@ public ConcertDetailResponse setConcertTicketingTime(ConcertTicketTimeSetRequest
if(ticketEndTime.isAfter(concert.getEndDate().atTime(LocalTime.MAX))) throw new BusinessException(ConcertErrorCode.CONCERT_TICKETING_END_TIME_IS_NOT_AFTER_CONCERT_END_DATE);

concert.ticketTimeSet(ticketTime, ticketEndTime);
// DB에 저장
Concert savedConcert = concertRepository.save(concert);
// 캐시에도 갱신
concertRedisRepository.updateCachedTickingDate(concert.getConcertId(),ticketTime,ticketEndTime);
return concertRepository.getConcertDetailById(savedConcert.getConcertId());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestClient;
Expand Down Expand Up @@ -112,7 +110,7 @@ public void setConcertsList() throws InterruptedException {
totalConcertsList.add(p);
}
log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중...");
Thread.sleep(200);
Thread.sleep(120);
}

}catch (Exception e){
Expand All @@ -126,7 +124,9 @@ public void setConcertsList() throws InterruptedException {
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
try {
for (ConcertListElement performanceListElement : totalConcertsList) {
// API에서 공연 상세 가져오기
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
Thread.sleep(120);
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();

// 콘서트 위치 저장
Expand All @@ -137,6 +137,7 @@ public void setConcertsList() throws InterruptedException {
if (concertPlace == null) {
// 맵이나 DB에 없다면 API에서 해당 콘서트 위치를 가져와서 DB에 저장 후 캐시에 저장
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
Thread.sleep(120);
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
concertPlace = concertPlaceDetail.getConcertPlace();
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
Expand Down Expand Up @@ -167,8 +168,6 @@ public void setConcertsList() throws InterruptedException {

addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
addedConcertImages += saveConcertImages(concertDetail, savedConcert);

Thread.sleep(300);
}
} catch (Exception e) {
log.error("개별 공연 세부 내용 저장 도중 오류 발생");
Expand Down Expand Up @@ -286,6 +285,7 @@ public SetResultResponse updateConcertData() throws InterruptedException {
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
imageRepository.deleteByConcertId(savedConcert.getConcertId());
// 갱신된 데이터로 연관 테이블 저장
updatedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
updatedConcertImages += saveConcertImages(concertDetail, savedConcert);
}
Expand All @@ -296,7 +296,7 @@ public SetResultResponse updateConcertData() throws InterruptedException {
// 갱신 후 업데이트 시간 저장, 캐시의 데이터 삭제
concertUpdateTimeRepository.save(updatedTime);
concertRedisRepository.deleteAllConcertsList();
concertRedisRepository.deleteAllConcertDetail();
concertRedisRepository.deleteAllCachedConcertDetail();
concertRedisRepository.deleteTotalConcertsCount(ListSort.VIEW);
return new SetResultResponse(addedConcerts,updatedConcerts,addedConcertPlaces,updatedConcertPlaces,addedConcertImages,updatedConcertImages,addedTicketOffices,updatedTicketOffices);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class ConcertScheduler {
private final ConcertNotifyService concertNotifyService;

// 공연 데이터 업데이트를 진행합니다.
@Scheduled(cron = "0 0 2 * * *")
@Scheduled(cron = "0 0 2 * * MON")
public void concertUpdateSchedule() throws InterruptedException {
kopisApiService.updateConcertData();
}
Expand Down