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

DB 부하 최소화 #3

Merged
merged 13 commits into from
Sep 6, 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 @@ -66,6 +66,12 @@ public void makeApply(@RequestHeader("ApplyTicket") String applyTicket,
orderEventCommandService.makeOrderEventWinner(applyTicket,eventId,orderEventWinnerRequestDto);
}

@PostMapping(path="/event/order/refresh")
public void refreshOrderEvent(){
orderResultCommandService.refreshApplyCount();
}


@ExceptionHandler(WrongPhoneNumberFormatException.class)
public ResponseEntity<ErrorResponse> handleWrongPhoneNumberFormatException(WrongPhoneNumberFormatException wrongPhoneNumberFormatException){
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(ErrorResponse.of(wrongPhoneNumberFormatException.getMessage()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLTransientConnectionException;
import java.util.Stack;

@Service
@RequiredArgsConstructor
Expand All @@ -22,13 +24,10 @@ public class OrderResultSaveService {
private final OrderResultRepository orderResultRepository;
private final CurrentOrderEventManageService currentOrderEventManageService;

@Qualifier("orderResultDatasource") //timeOut이 다른 커넥션을 가져온다.
@Getter
private final HikariDataSource dataSource;

@Transactional(transactionManager = "orderResultTransactionManager") //transactional 매니저도 변경
public boolean isOrderApplyNotFullThenSaveConnectionOpen(String applyToken) throws CannotCreateTransactionException {
if( currentOrderEventManageService.isOrderApplyNotFullThenPlusCount()){
@Transactional(transactionManager = "orderResultTransactionManager")
public boolean isOrderApplyNotFullThenSaveConnectionOpen(String applyToken,int applyCountIndex) throws CannotCreateTransactionException {
if( currentOrderEventManageService.isOrderApplyNotFullThenPlusCount(applyCountIndex)){
OrderResult orderResult = OrderResult.makeOrderEventApply(applyToken);
saveOrderResult(orderResult);
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.watermelon.server.orderResult.config;


import com.watermelon.server.orderResult.service.BlockingQueueIndexLoadBalanceService;
import com.watermelon.server.orderResult.service.IndexLoadBalanceService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ArrayBlockingQueue;

@Configuration
public class IndexLoadBalanceConfig {
@Bean
public IndexLoadBalanceService indexLoadBalanceService() {
return new BlockingQueueIndexLoadBalanceService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class OrderApplyCount {
@Setter
private int count;

boolean isFull;

public static OrderApplyCount create(OrderEvent orderEvent) {
return OrderApplyCount.builder()
.build();
Expand All @@ -37,5 +39,12 @@ public void addCount(){

public void clearCount(){
this.count = 0;
this.isFull = false;
}
public void makeFull(){
this.isFull = true;
}
public void makeNotFull(){
this.isFull = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;
Expand All @@ -18,8 +19,17 @@ public interface OrderApplyCountRepository extends JpaRepository<OrderApplyCount
@QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")
})
@Query("select oac from OrderApplyCount oac order by oac.id asc limit 1")
Optional<OrderApplyCount> findWithExclusiveLock();
Optional<OrderApplyCount> findLimitOneExclusiveLock();


@Lock(LockModeType.PESSIMISTIC_WRITE) //InnoDb는 레코드락만 건다
@QueryHints({
@QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")
})
@Query("select oac from OrderApplyCount oac where oac.id = :id")
Optional<OrderApplyCount> findWithIdExclusiveLock(@Param("id") Long id);

@Query("select oac from OrderApplyCount oac order by oac.id asc limit 1")
Optional<OrderApplyCount> findCurrent();
Optional<OrderApplyCount> findFirstApplyCountById();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.watermelon.server.orderResult.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

@Service
public class BlockingQueueIndexLoadBalanceService implements IndexLoadBalanceService {


private BlockingQueue<Integer> blockingQueue;
public BlockingQueueIndexLoadBalanceService() {
this.blockingQueue = new ArrayBlockingQueue<>(1000);
}
@Override
public int getIndex() {
return blockingQueue.poll();
}
@Override
public void addIndexToQueue(int totalIndexCount, int maxIndexNumber) {
for(int index=0;index<totalIndexCount;index++){
blockingQueue.add((index%maxIndexNumber));
}
}

@Override
public void refreshQueue() {
this.blockingQueue.clear();

}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.watermelon.server.orderResult.service;

import org.springframework.stereotype.Service;



@Service
public class ConcurrentQueueIndexLoadBalanceService implements IndexLoadBalanceService {

@Override
public int getIndex() {
return 0;
}

@Override
public void addIndexToQueue(int totalIndexCount, int maxIndexNumber) {

}

@Override
public void refreshQueue() {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,85 +7,161 @@
import com.watermelon.server.order.exception.WrongOrderEventFormatException;
import com.watermelon.server.orderResult.repository.OrderApplyCountRepository;
import com.watermelon.server.orderResult.domain.OrderApplyCount;
import com.zaxxer.hikari.HikariDataSource;
import lombok.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.ArrayList;
import java.util.List;


@Service
@RequiredArgsConstructor
public class CurrentOrderEventManageService {
private static final Logger log = LoggerFactory.getLogger(CurrentOrderEventManageService.class);
@Getter
private OrderEvent currentOrderEvent;

@Getter
private volatile OrderEvent orderEventFromServerMemory;
private final OrderApplyCountRepository orderApplyCountRepository;

@Qualifier("orderResultDatasource") //timeOut이 다른 커넥션을 가져온다.
private final HikariDataSource dataSource;
private final IndexLoadBalanceService indexLoadBalanceService;

@Getter
@Setter
private List<OrderApplyCount> orderApplyCountsFromServerMemory = new ArrayList<>();

@Transactional(transactionManager = "orderResultTransactionManager")
public boolean isOrderApplyNotFullThenPlusCount(){
public boolean isOrderApplyNotFullThenPlusCount(int applyCountIndex){
if(isOrderApplyFull()) {
return false;
}
Optional<OrderApplyCount> orderApplyCountOptional = orderApplyCountRepository.findWithExclusiveLock();
OrderApplyCount orderApplyCount = orderApplyCountOptional.get();
if(currentOrderEvent.getWinnerCount()- orderApplyCount.getCount()>0){
orderApplyCount.addCount();
orderApplyCountRepository.save(orderApplyCount);
return true;
}
/**
* DB에 저장되어서 Lock을 걸고 가져오는 OrderApplyCount와
* 서버에 저장되어있는 OrderApplyCount를 분리하여 관리
*/
try{
OrderApplyCount orderApplyCountFromServerMemory = null;
OrderApplyCount orderApplyCountFromDB =null;

// 여기서 CLOSED로 바꿀지 언정 실제 DB에는 저장되지 않음(currentOrderEvent는 DB에서 꺼내온 정보가 아님)
// 이 CLOSED는 REDIS를 읽는 작업을 줄여주기 위한 변수용
this.currentOrderEvent.setOrderEventStatus(OrderEventStatus.CLOSED);
return false;
/**
* 배정받은 현재 접근해야하는 ApplyCount의 Index에
* 서버에 저장되어있는 ApplyCount 목록에서 해당 Index의 ApplyCount를 가져온다
* 그 이후에 해당 ApplyCount의 ID로 DB에 비관적 락을 걸고 접근한다.
*/
orderApplyCountFromServerMemory = orderApplyCountsFromServerMemory.get(applyCountIndex);
if(orderApplyCountFromServerMemory.isFull()) return false;

orderApplyCountFromDB =
orderApplyCountRepository.findWithIdExclusiveLock(orderApplyCountFromServerMemory.getId()).get();

int eachMaxWinnerCount = orderEventFromServerMemory.getWinnerCount()/orderApplyCountsFromServerMemory.size();
if(eachMaxWinnerCount > orderApplyCountFromDB.getCount()){
orderApplyCountFromDB.addCount();
orderApplyCountRepository.save(orderApplyCountFromDB);
/**
* 만약 각 ApplyCount에 정해진 개수만큼 꽉 찼다면
* 해당 ApplyCount의 flag를 full로 만들어준다.
*/
if(eachMaxWinnerCount == orderApplyCountFromDB.getCount()){
orderApplyCountFromServerMemory.makeFull();
orderApplyCountFromDB.makeFull();
orderApplyCountRepository.save(orderApplyCountFromDB);
}
return true;
}
return false;
}
finally {
/**
* 모든 ApplyCount의 flag가 full이라면 현재 이벤트의 상태 flag를 바꾼다
*/
boolean allFull = true;
for(OrderApplyCount eachOrderApplyCount : orderApplyCountsFromServerMemory){
if(!eachOrderApplyCount.isFull()){
allFull = false;
}
}
if(allFull){orderEventFromServerMemory.setOrderEventStatus(OrderEventStatus.CLOSED);}
}
}

@Transactional
public void refreshOrderEventInProgress(OrderEvent orderEventFromDB){
//동일한 이벤트라면
if(currentOrderEvent != null && orderEventFromDB.getId().equals(currentOrderEvent.getId())){
if(getCurrentApplyCount()<currentOrderEvent.getWinnerCount()){
this.currentOrderEvent.setOrderEventStatus(OrderEventStatus.OPEN);
}
//실제 DB에 CLOSED로 바꾸어주는 메소드는 이곳 (스케쥴링)
orderEventFromDB.setOrderEventStatus(currentOrderEvent.getOrderEventStatus());
/**
* 서버에 임시적으로 저장되어있는 OrderEvent와 DB에서 온 인자의 OrderEvent가 같다면
* 실제 DB에 서버에 임시적으로 저장되어있는 OrderEvent의 상태를 덮어씌운다.
*
* 하지만 다르다면 서버에 저장되어있는 OrderEvent를 최신화 시켜주고
* 당첨자 수 또한 초기화 시켜준다.
*/
if(orderEventFromServerMemory != null && orderEventFromDB.getId().equals(orderEventFromServerMemory.getId())){
// if(!checkIfApplyCountFull()){
// orderEventFromServerMemory.setOrderEventStatus(OrderEventStatus.OPEN);
// }
orderEventFromDB.setOrderEventStatus(orderEventFromServerMemory.getOrderEventStatus());
return;
}
currentOrderEvent = orderEventFromDB;
clearOrderApplyCount();
orderEventFromServerMemory = orderEventFromDB;
refreshApplyCount();
}



@Transactional(transactionManager = "orderResultTransactionManager")
public int getCurrentApplyCount() {
if(orderApplyCountRepository.findAll().isEmpty()) orderApplyCountRepository.save(OrderApplyCount.builder().build());
return orderApplyCountRepository.findCurrent().get().getCount();
public void refreshApplyCount() {
clearOrderApplyCount();
indexLoadBalanceService.refreshQueue();
indexLoadBalanceService.addIndexToQueue(orderEventFromServerMemory.getWinnerCount(), orderApplyCountsFromServerMemory.size());
orderEventFromServerMemory.setOrderEventStatus(OrderEventStatus.OPEN);
}

// @Transactional(transactionManager = "orderResultTransactionManager")
// public boolean checkIfApplyCountFull() {
// boolean isAllApplyCountFull = true;
// int remainWinnerCount = 0;
// List<OrderApplyCount> orderApplyCountsFromDB = orderApplyCountRepository.findAll();
// for(OrderApplyCount eachOrderApplyCount : orderApplyCountsFromDB){
// int eachMaxWinnerCount = orderEventFromServerMemory.getWinnerCount()/orderApplyCountsFromDB.size();
// int eachRemainWinnerCount = eachOrderApplyCount.getCount();
// if(eachRemainWinnerCount<eachMaxWinnerCount){
// if(eachOrderApplyCount.isFull()){
// remainWinnerCount += (eachMaxWinnerCount-eachRemainWinnerCount);
// log.info("remain Winner Count = {}" ,remainWinnerCount);
// eachOrderApplyCount.makeNotFull();
// isAllApplyCountFull = false;
// orderApplyCountRepository.save(eachOrderApplyCount);
// }
// }
// }
// indexLoadBalanceService.addIndexToQueue(remainWinnerCount, orderApplyCountsFromDB.size());
// orderApplyCountsFromServerMemory = orderApplyCountsFromDB;
// return isAllApplyCountFull;
// }
//

@Transactional(transactionManager = "orderResultTransactionManager")
public void clearOrderApplyCount() {
orderApplyCountRepository.findCurrent().get().clearCount();
/**
* 서버에 저장되고 있는 이벤트가 바뀔 때마다 실행되는 메소드
* 1. 모든 ApplyCount 레코드들을 초기화 시켜주고
* 2. 현재 요청이 들어온다면 접근해야하는 ApplyCount의 ID의 인덱스가 저장되어있는 변수를 초기화 해준다.
*/
orderApplyCountsFromServerMemory = orderApplyCountRepository.findAll();
orderApplyCountsFromServerMemory.forEach(OrderApplyCount::clearCount);
}

public boolean checkPrevious(String submitAnswer){
if(currentOrderEvent.getQuiz().isCorrect(submitAnswer)) return true;
if(orderEventFromServerMemory.getQuiz().isCorrect(submitAnswer)) return true;
return false;
}
public boolean isTimeInEvent(LocalDateTime now){
if(now.isAfter(currentOrderEvent.getStartDate()) &&now.isBefore(currentOrderEvent.getEndDate())) return true;
if(now.isAfter(orderEventFromServerMemory.getStartDate()) &&now.isBefore(orderEventFromServerMemory.getEndDate())) return true;
return false;
}
public boolean isEventAndQuizIdWrong( Long eventId,Long quizId) {
if(currentOrderEvent !=null && currentOrderEvent.getId().equals(eventId) && currentOrderEvent.getQuiz().getId().equals(quizId)) return true;
if(orderEventFromServerMemory !=null && orderEventFromServerMemory.getId().equals(eventId) && orderEventFromServerMemory.getQuiz().getId().equals(quizId)) return true;
return false;
}
public void checkingInfoErrors( Long eventId, Long quizId)
Expand All @@ -94,12 +170,14 @@ public void checkingInfoErrors( Long eventId, Long quizId)
if (!isTimeInEvent(LocalDateTime.now())) throw new NotDuringEventPeriodException();
}
public Long getCurrentOrderEventId() {
if (currentOrderEvent == null) return null;
return this.currentOrderEvent.getId();
if (orderEventFromServerMemory == null) return null;
return this.orderEventFromServerMemory.getId();
}

public boolean isOrderApplyFull() {
if(currentOrderEvent.getOrderEventStatus().equals(OrderEventStatus.CLOSED))return true;
if(orderEventFromServerMemory.getOrderEventStatus().equals(OrderEventStatus.CLOSED)){
return true;
}
return false;
}
}
Loading
Loading