diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawService.java b/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawService.java index a579b7cb..5d7fcb78 100644 --- a/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawService.java +++ b/src/main/java/com/softeer/backend/fo_domain/draw/service/DrawService.java @@ -9,6 +9,10 @@ import com.softeer.backend.fo_domain.draw.exception.DrawException; import com.softeer.backend.fo_domain.draw.repository.DrawParticipationInfoRepository; import com.softeer.backend.fo_domain.draw.repository.DrawRepository; +import com.softeer.backend.fo_domain.draw.test.lua.DrawRedisLuaTest; +import com.softeer.backend.fo_domain.draw.test.tablelock.DrawDatabaseTest; +import com.softeer.backend.fo_domain.draw.test.tablelock.DrawDatabaseUtil; +import com.softeer.backend.fo_domain.draw.test.withoutlock.DrawDatabaseWithoutLockTest; import com.softeer.backend.fo_domain.draw.util.DrawAttendanceCountUtil; import com.softeer.backend.fo_domain.draw.util.DrawRemainDrawCountUtil; import com.softeer.backend.fo_domain.draw.util.DrawResponseGenerateUtil; @@ -38,8 +42,11 @@ public class DrawService { private final DrawResponseGenerateUtil drawResponseGenerateUtil; private final DrawAttendanceCountUtil drawAttendanceCountUtil; private final DrawSettingManager drawSettingManager; + private final DrawDatabaseUtil drawDatabaseUtil; private final DrawRepository drawRepository; - private final DrawRemainDrawCountUtil drawRemainDrawCountUtil; + private final DrawDatabaseTest drawDatabaseTest; + private final DrawDatabaseWithoutLockTest drawDatabaseWithoutLockTest; + private final DrawRedisLuaTest drawRedisLuaTest; /** * 1. 연속 참여일수 조회 @@ -61,8 +68,9 @@ public DrawMainResponseDto getDrawMainPageInfo(Integer userId) { drawAttendanceCount = drawAttendanceCountUtil.handleAttendanceCount(userId, drawParticipationInfo); } int invitedNum = shareInfo.getInvitedNum(); - // 새로 접속 시 남은 추첨 횟수 증가시켜주는 로직 - int remainDrawCount = drawRemainDrawCountUtil.handleRemainDrawCount(userId, shareInfo.getRemainDrawCount(), drawParticipationInfo); + int remainDrawCount = shareInfo.getRemainDrawCount(); + + System.out.println("Draw Attendance = " + drawAttendanceCount); if (drawAttendanceCount >= 7) { // 7일 연속 출석자라면 @@ -89,61 +97,69 @@ public DrawMainResponseDto getDrawMainPageInfo(Integer userId) { * 6-2. 낙첨자일 경우 해당 사용자의 낙첨 횟수 증가, 낙첨 응답 반환 */ public DrawModalResponseDto participateDrawEvent(Integer userId) { - // 복권 기회 조회 - ShareInfo shareInfo = shareInfoRepository.findShareInfoByUserId(userId) - .orElseThrow(() -> new ShareInfoException(ErrorStatus._NOT_FOUND)); - - // 만약 남은 참여 기회가 0이라면 - if (shareInfo.getRemainDrawCount() == 0) { - throw new DrawException(ErrorStatus._NOT_FOUND); - } + // 락을 이용한 데이터베이스 테스트 + // return drawDatabaseTest.participateDrawEvent(userId); - drawRedisUtil.increaseDrawParticipationCount(); // 추첨 이벤트 참여자수 증가 - shareInfoRepository.decreaseRemainDrawCount(userId); // 횟수 1회 차감 + // Lua Script를 이용한 redis 테스트 + return drawRedisLuaTest.participateDrawEvent(userId); - // 만약 당첨 목록에 존재한다면 이미 오늘은 한 번 당첨됐다는 뜻이므로 LoseModal 반환 - int ranking = drawRedisUtil.getRankingIfWinner(userId); // 당첨 목록에 존재한다면 랭킹 반환 - if (ranking != 0) { - drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 - return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 - } - // 당첨자 수 조회 - int first = drawSettingManager.getWinnerNum1(); // 1등 수 - int second = drawSettingManager.getWinnerNum2(); // 2등 수 - int third = drawSettingManager.getWinnerNum3(); // 3등 수 - - // 당첨자 수 설정 - drawUtil.setFirst(first); - drawUtil.setSecond(second); - drawUtil.setThird(third); - - // 추첨 로직 실행 - drawUtil.performDraw(); - - if (drawUtil.isDrawWin()) { // 당첨자일 경우 - ranking = drawUtil.getRanking(); - int winnerNum; - if (ranking == 1) { - winnerNum = first; - } else if (ranking == 2) { - winnerNum = second; - } else { - winnerNum = third; - } - - if (drawRedisUtil.isWinner(userId, ranking, winnerNum)) { // 레디스에 추첨 티켓이 남았다면, 레디스 당첨 목록에 추가 - drawParticipationInfoRepository.increaseWinCount(userId); // 당첨 횟수 증가 - return drawResponseGenerateUtil.generateDrawWinnerResponse(ranking); // WinModal 반환 - } else { - // 추첨 티켓이 다 팔렸다면 로직상 당첨자라도 실패 반환 - drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 - return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 - } - } else { // 낙첨자일 경우 - drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 - return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 - } + // 복권 기회 조회 +// ShareInfo shareInfo = shareInfoRepository.findShareInfoByUserId(userId) +// .orElseThrow(() -> new ShareInfoException(ErrorStatus._NOT_FOUND)); +// +// // 만약 남은 참여 기회가 0이라면 +// if (shareInfo.getRemainDrawCount() == 0) { +// throw new DrawException(ErrorStatus._NOT_FOUND); +// } +// +// drawRedisUtil.increaseDrawParticipationCount(); // 추첨 이벤트 참여자수 증가 +// shareInfoRepository.decreaseRemainDrawCount(userId); // 횟수 1회 차감 +// +// // 만약 당첨 목록에 존재한다면 이미 오늘은 한 번 당첨됐다는 뜻이므로 LoseModal 반환 +// int ranking = drawRedisUtil.getRankingIfWinner(userId); // 당첨 목록에 존재한다면 랭킹 반환 +// if (ranking != 0) { +// drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 +// return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 +// } +// +// // 당첨자 수 조회 +// int first = drawSettingManager.getWinnerNum1(); // 1등 수 +// int second = drawSettingManager.getWinnerNum2(); // 2등 수 +// int third = drawSettingManager.getWinnerNum3(); // 3등 수 +// +// // 당첨자 수 설정 +// drawUtil.setFirst(first); +// drawUtil.setSecond(second); +// drawUtil.setThird(third); +// +// // 추첨 로직 실행 +// drawUtil.performDraw(); +// +// if (drawUtil.isDrawWin()) { // 당첨자일 경우 +// ranking = drawUtil.getRanking(); +// int winnerNum; +// if (ranking == 1) { +// winnerNum = first; +// } else if (ranking == 2) { +// winnerNum = second; +// } else { +// winnerNum = third; +// } +// +// if (drawRedisUtil.isWinner(userId, ranking, winnerNum)) { // 레디스에 추첨 티켓이 남았다면, 레디스 당첨 목록에 추가 +// // 추첨 티켓이 다 팔리지 않았다면 +// drawParticipationInfoRepository.increaseWinCount(userId); // 당첨 횟수 증가 +// return drawResponseGenerateUtil.generateDrawWinnerResponse(ranking); // WinModal 반환 +// } else { +// // 추첨 티켓이 다 팔렸다면 로직상 당첨자라도 실패 반환 +// drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 +// return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 +// } +// } else { // 낙첨자일 경우 +// drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 +// return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 +// } } /** diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerList.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerList.java new file mode 100644 index 00000000..3b4d12c9 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerList.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "draw_test_first_winner_list") +public class DrawTestFirstWinnerList { + @Id + @Column(name = "user_id") + Integer userId; +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerListRepository.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerListRepository.java new file mode 100644 index 00000000..1de91df0 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestFirstWinnerListRepository.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DrawTestFirstWinnerListRepository extends JpaRepository { + boolean existsByUserId(Integer userId); + + long count(); + + // 또는 JPA 엔티티에 대해 비관적 잠금을 직접 적용 + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT d FROM DrawTestFirstWinnerList d") + List findAllForUpdate(); +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCount.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCount.java new file mode 100644 index 00000000..90dd1f20 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCount.java @@ -0,0 +1,23 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "draw_test_participant_count") +public class DrawTestParticipantCount { + @Id + @Column(name = "participant_count_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + Integer participantCountId; + + @Column(name = "count") + Integer count; +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCountRepository.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCountRepository.java new file mode 100644 index 00000000..7dfe74f8 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestParticipantCountRepository.java @@ -0,0 +1,15 @@ +package com.softeer.backend.fo_domain.draw.test; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface DrawTestParticipantCountRepository extends JpaRepository { + @Transactional + @Modifying + @Query("UPDATE DrawTestParticipantCount p SET p.count = p.count + 1 WHERE p.participantCountId = 1") + void increaseParticipantCount(); +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerList.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerList.java new file mode 100644 index 00000000..7849b05d --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerList.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "draw_test_second_winner_list") +public class DrawTestSecondWinnerList { + @Id + @Column(name = "user_id") + Integer userId; +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerListRepository.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerListRepository.java new file mode 100644 index 00000000..3a145600 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestSecondWinnerListRepository.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DrawTestSecondWinnerListRepository extends JpaRepository { + boolean existsByUserId(Integer userId); + + long count(); + + // 또는 JPA 엔티티에 대해 비관적 잠금을 직접 적용 + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT d FROM DrawTestSecondWinnerList d") + List findAllForUpdate(); +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerList.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerList.java new file mode 100644 index 00000000..250c6909 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerList.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "draw_test_third_winner_list") +public class DrawTestThirdWinnerList { + @Id + @Column(name = "user_id") + Integer userId; +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerListRepository.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerListRepository.java new file mode 100644 index 00000000..c5f06f7e --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/DrawTestThirdWinnerListRepository.java @@ -0,0 +1,22 @@ +package com.softeer.backend.fo_domain.draw.test; + +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DrawTestThirdWinnerListRepository extends JpaRepository { + boolean existsByUserId(Integer userId); + + long count(); + + // 또는 JPA 엔티티에 대해 비관적 잠금을 직접 적용 + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT d FROM DrawTestThirdWinnerList d") + List findAllForUpdate(); +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/ParticipateInterface.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/ParticipateInterface.java new file mode 100644 index 00000000..c10bbe16 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/ParticipateInterface.java @@ -0,0 +1,8 @@ +package com.softeer.backend.fo_domain.draw.test; + +import com.softeer.backend.fo_domain.draw.dto.participate.DrawModalResponseDto; +import org.springframework.stereotype.Component; + +public interface ParticipateInterface { + public DrawModalResponseDto participateDrawEvent(Integer userId); +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaTest.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaTest.java new file mode 100644 index 00000000..297c1d8f --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaTest.java @@ -0,0 +1,89 @@ +package com.softeer.backend.fo_domain.draw.test.lua; + +import com.softeer.backend.fo_domain.draw.dto.participate.DrawModalResponseDto; +import com.softeer.backend.fo_domain.draw.repository.DrawParticipationInfoRepository; +import com.softeer.backend.fo_domain.draw.repository.DrawRepository; +import com.softeer.backend.fo_domain.draw.service.DrawSettingManager; +import com.softeer.backend.fo_domain.draw.util.DrawAttendanceCountUtil; +import com.softeer.backend.fo_domain.draw.util.DrawResponseGenerateUtil; +import com.softeer.backend.fo_domain.draw.util.DrawUtil; +import com.softeer.backend.fo_domain.share.domain.ShareInfo; +import com.softeer.backend.fo_domain.share.exception.ShareInfoException; +import com.softeer.backend.fo_domain.share.repository.ShareInfoRepository; +import com.softeer.backend.global.common.code.status.ErrorStatus; +import com.softeer.backend.global.util.DrawRedisUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DrawRedisLuaTest { + private final DrawParticipationInfoRepository drawParticipationInfoRepository; + private final ShareInfoRepository shareInfoRepository; + private final DrawRedisUtil drawRedisUtil; + private final DrawUtil drawUtil; + private final DrawResponseGenerateUtil drawResponseGenerateUtil; + private final DrawAttendanceCountUtil drawAttendanceCountUtil; + private final DrawSettingManager drawSettingManager; + private final DrawRepository drawRepository; + private final DrawRedisLuaUtil drawRedisLuaUtil; + + public DrawModalResponseDto participateDrawEvent(Integer userId) { + // 복권 기회 조회 + ShareInfo shareInfo = shareInfoRepository.findShareInfoByUserId(userId) + .orElseThrow(() -> new ShareInfoException(ErrorStatus._NOT_FOUND)); + + // 만약 남은 참여 기회가 0이라면 + if (shareInfo.getRemainDrawCount() == 0) { + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); + } + + drawRedisLuaUtil.increaseDrawParticipationCount(); // 추첨 이벤트 참여자수 증가 + shareInfoRepository.decreaseRemainDrawCount(userId); // 횟수 1회 차감 + + // 만약 당첨 목록에 존재한다면 이미 오늘은 한 번 당첨됐다는 뜻이므로 LoseModal 반환 + int ranking = drawRedisLuaUtil.getRankingIfWinner(userId); // 당첨 목록에 존재한다면 랭킹 반환 + if (ranking != 0) { + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + + // 당첨자 수 조회 + int first = drawSettingManager.getWinnerNum1(); // 1등 수 + int second = drawSettingManager.getWinnerNum2(); // 2등 수 + int third = drawSettingManager.getWinnerNum3(); // 3등 수 + + // 당첨자 수 설정 + drawUtil.setFirst(first); + drawUtil.setSecond(second); + drawUtil.setThird(third); + + // 추첨 로직 실행 + drawUtil.performDraw(); + + if (drawUtil.isDrawWin()) { // 당첨자일 경우 + ranking = drawUtil.getRanking(); + int winnerNum; + if (ranking == 1) { + winnerNum = first; + } else if (ranking == 2) { + winnerNum = second; + } else { + winnerNum = third; + } + + if (drawRedisLuaUtil.isWinner(userId, ranking, winnerNum)) { // 레디스에 추첨 티켓이 남았다면, 레디스 당첨 목록에 추가 + // 추첨 티켓이 다 팔리지 않았다면 + drawParticipationInfoRepository.increaseWinCount(userId); // 당첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawWinnerResponse(ranking); // WinModal 반환 + } else { + // 추첨 티켓이 다 팔렸다면 로직상 당첨자라도 실패 반환 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } else { // 낙첨자일 경우 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaUtil.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaUtil.java new file mode 100644 index 00000000..6e282682 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/DrawRedisLuaUtil.java @@ -0,0 +1,89 @@ +package com.softeer.backend.fo_domain.draw.test.lua; + +import com.softeer.backend.global.annotation.EventLock; +import com.softeer.backend.global.common.constant.RedisKeyPrefix; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Component +@RequiredArgsConstructor +public class DrawRedisLuaUtil { + private final RedisTemplate integerRedisTemplate; + private final RedisScript isWinnerScript; + private final RedisScript checkIfUserInSetScript; + + // 추첨 당첨자 목록: DRAW_WINNER_LIST_{ranking}, Set + // 추첨 참여자 수: DRAW_PARTICIPANT_COUNT, Integer + + // ranking의 추첨 당첨자 목록 반환 + public Set getAllDataAsSet(String key) { + return integerRedisTemplate.opsForSet().members(key); + } + + private Long getIntegerSetSize(String key) { + return integerRedisTemplate.opsForSet().size(key); + } + + // ranking의 당첨자 목록 업데이트 + public void setIntegerValueToSet(String key, Integer userId) { + integerRedisTemplate.opsForSet().add(key, userId); + } + + /** + * userId가 당첨자 목록에 있으면 등수, 없으면 0 반환 + * + * @param userId 사용자 아이디 + */ + public int getRankingIfWinner(Integer userId) { + for (int ranking = 1; ranking < 4; ranking++) { + String drawWinnerKey = RedisKeyPrefix.DRAW_WINNER_LIST_PREFIX.getPrefix() + ranking; + + Long result = integerRedisTemplate.execute( + checkIfUserInSetScript, + List.of(drawWinnerKey), // KEYS + userId.toString() // ARGV + ); + + if (result != null && result == 1) { + return ranking; + } + } + return 0; + } + + /** + * 해당 등수의 자리가 남아있는지 판단하는 메서드 + *

+ * 1. redis에서 해당 등수의 자리가 남아있는지 판단한다. + * 1-1. 자리가 남아있다면 사용자를 당첨자 리스트에 저장하고 true 반환 + * 1-2. 자리가 없다면 false 반환 + */ + // @EventLock(key = "DRAW_WINNER") + public boolean isWinner(Integer userId, int ranking, int winnerNum) { + String drawWinnerKey = RedisKeyPrefix.DRAW_WINNER_LIST_PREFIX.getPrefix() + ranking; + + // Lua 스크립트를 실행하여 당첨자를 추가할지 결정 + Long result = integerRedisTemplate.execute( + isWinnerScript, + Collections.singletonList(drawWinnerKey), // KEYS + userId.toString(), String.valueOf(winnerNum) // ARGV + ); + + System.out.println(result == null); + + return result != null && result == 1; + } + + /** + * 추첨 참여자 수 증가 + */ + public void increaseDrawParticipationCount() { + integerRedisTemplate.opsForValue().increment(RedisKeyPrefix.DRAW_PARTICIPANT_COUNT_PREFIX.getPrefix()); + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/RedisConfigForLua.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/RedisConfigForLua.java new file mode 100644 index 00000000..c83e397d --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/lua/RedisConfigForLua.java @@ -0,0 +1,39 @@ +package com.softeer.backend.fo_domain.draw.test.lua; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; + +@Configuration +public class RedisConfigForLua { + @Bean + public RedisScript isWinnerScript() { + String script = "local draw_winner_key = KEYS[1] " + + "local user_id = ARGV[1] " + + "local winner_num = tonumber(ARGV[2]) " + + "local winner_set_size = redis.call('SCARD', draw_winner_key) " + + "if winner_set_size < winner_num then " + + " redis.call('SADD', draw_winner_key, user_id) " + + " return 1 " + + "else " + + " return 0 " + + "end"; + + return new DefaultRedisScript<>(script, Long.class); + } + + @Bean + public RedisScript checkIfUserInSetScript() { + String script = "local draw_winner_key = KEYS[1] " + + "local user_id = ARGV[1] " + + "local exists = redis.call('SISMEMBER', draw_winner_key, user_id) " + + "if exists == 1 then " + + " return 1 " + + "else " + + " return 0 " + + "end"; + + return new DefaultRedisScript<>(script, Long.class); + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseTest.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseTest.java new file mode 100644 index 00000000..e2037a1e --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseTest.java @@ -0,0 +1,83 @@ +package com.softeer.backend.fo_domain.draw.test.tablelock; + +import com.softeer.backend.fo_domain.draw.dto.participate.DrawModalResponseDto; +import com.softeer.backend.fo_domain.draw.repository.DrawParticipationInfoRepository; +import com.softeer.backend.fo_domain.draw.service.DrawSettingManager; +import com.softeer.backend.fo_domain.draw.util.DrawResponseGenerateUtil; +import com.softeer.backend.fo_domain.draw.util.DrawUtil; +import com.softeer.backend.fo_domain.share.domain.ShareInfo; +import com.softeer.backend.fo_domain.share.exception.ShareInfoException; +import com.softeer.backend.fo_domain.share.repository.ShareInfoRepository; +import com.softeer.backend.global.common.code.status.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DrawDatabaseTest { + private final DrawParticipationInfoRepository drawParticipationInfoRepository; + private final ShareInfoRepository shareInfoRepository; + private final DrawDatabaseUtil drawDatabaseUtil; + private final DrawUtil drawUtil; + private final DrawResponseGenerateUtil drawResponseGenerateUtil; + private final DrawSettingManager drawSettingManager; + + public DrawModalResponseDto participateDrawEvent(Integer userId) { + // 복권 기회 조회 + ShareInfo shareInfo = shareInfoRepository.findShareInfoByUserId(userId) + .orElseThrow(() -> new ShareInfoException(ErrorStatus._NOT_FOUND)); + + // 만약 남은 참여 기회가 0이라면 + if (shareInfo.getRemainDrawCount() == 0) { + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); + } + + drawDatabaseUtil.increaseDrawParticipationCount(); // 추첨 이벤트 참여자수 증가 + shareInfoRepository.decreaseRemainDrawCount(userId); // 횟수 1회 차감 + + // 만약 당첨 목록에 존재한다면 이미 오늘은 한 번 당첨됐다는 뜻이므로 LoseModal 반환 + int ranking = drawDatabaseUtil.getRankingIfWinner(userId); // 당첨 목록에 존재한다면 랭킹 반환 + if (ranking != 0) { + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + + // 당첨자 수 조회 + int first = drawSettingManager.getWinnerNum1(); // 1등 수 + int second = drawSettingManager.getWinnerNum2(); // 2등 수 + int third = drawSettingManager.getWinnerNum3(); // 3등 수 + + // 당첨자 수 설정 + drawUtil.setFirst(first); + drawUtil.setSecond(second); + drawUtil.setThird(third); + + // 추첨 로직 실행 + drawUtil.performDraw(); + + if (drawUtil.isDrawWin()) { // 당첨자일 경우 + ranking = drawUtil.getRanking(); + int winnerNum; + if (ranking == 1) { + winnerNum = first; + } else if (ranking == 2) { + winnerNum = second; + } else { + winnerNum = third; + } + + if (drawDatabaseUtil.isWinner(userId, ranking, winnerNum)) { // 레디스에 추첨 티켓이 남았다면, 레디스 당첨 목록에 추가 + // 추첨 티켓이 다 팔리지 않았다면 + drawParticipationInfoRepository.increaseWinCount(userId); // 당첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawWinnerResponse(ranking); // WinModal 반환 + } else { + // 추첨 티켓이 다 팔렸다면 로직상 당첨자라도 실패 반환 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } else { // 낙첨자일 경우 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseUtil.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseUtil.java new file mode 100644 index 00000000..49f3bfd2 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/tablelock/DrawDatabaseUtil.java @@ -0,0 +1,103 @@ +package com.softeer.backend.fo_domain.draw.test.tablelock; + +import com.softeer.backend.fo_domain.draw.test.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class DrawDatabaseUtil { + private final DrawTestFirstWinnerListRepository drawTestFirstWinnerListRepository; + private final DrawTestSecondWinnerListRepository drawTestSecondWinnerListRepository; + private final DrawTestThirdWinnerListRepository drawTestThirdWinnerListRepository; + private final DrawTestParticipantCountRepository drawTestParticipantCountRepository; + + public void increaseDrawParticipationCount() { + drawTestParticipantCountRepository.increaseParticipantCount(); + } + + @Transactional + public int getRankingIfWinner(Integer userId) { + if (drawTestFirstWinnerListRepository.existsByUserId(userId)) { + return 1; + } + + if (drawTestSecondWinnerListRepository.existsByUserId(userId)) { + return 2; + } + + if (drawTestThirdWinnerListRepository.existsByUserId(userId)) { + return 3; + } + return 0; + } + + @Transactional(isolation = Isolation.SERIALIZABLE) + public boolean isWinner(Integer userId, int ranking, int winnerNum) { + if (ranking == 1) { + return isFirstWinner(userId, winnerNum); + } + if (ranking == 2) { + return isSecondWinner(userId, winnerNum); + } + if (ranking == 3) { + return isThirdWinner(userId, winnerNum); + } + return false; + } + + @Transactional + public boolean isFirstWinner(Integer userId, int winnerNum) { + // 비관적 잠금으로 데이터 획득 + List winners = drawTestFirstWinnerListRepository.findAllForUpdate(); + + if (winners.size() < winnerNum) { + DrawTestFirstWinnerList firstWinner = DrawTestFirstWinnerList.builder() + .userId(userId) + .build(); + + drawTestFirstWinnerListRepository.save(firstWinner); + return true; + } + + return false; + } + + @Transactional + public boolean isSecondWinner(Integer userId, int winnerNum) { + // 비관적 잠금으로 데이터 획득 + List winners = drawTestSecondWinnerListRepository.findAllForUpdate(); + + if (winners.size() < winnerNum) { + DrawTestSecondWinnerList secondWinner = DrawTestSecondWinnerList.builder() + .userId(userId) + .build(); + + drawTestSecondWinnerListRepository.save(secondWinner); + return true; + } + + return false; + } + + @Transactional + public boolean isThirdWinner(Integer userId, int winnerNum) { + // 비관적 잠금으로 데이터 획득 + List winners = drawTestThirdWinnerListRepository.findAllForUpdate(); + + if (winners.size() < winnerNum) { + DrawTestThirdWinnerList thirdWinner = DrawTestThirdWinnerList.builder() + .userId(userId) + .build(); + + drawTestThirdWinnerListRepository.save(thirdWinner); + return true; + } + + return false; + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockTest.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockTest.java new file mode 100644 index 00000000..6c4b8af5 --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockTest.java @@ -0,0 +1,83 @@ +package com.softeer.backend.fo_domain.draw.test.withoutlock; + +import com.softeer.backend.fo_domain.draw.dto.participate.DrawModalResponseDto; +import com.softeer.backend.fo_domain.draw.repository.DrawParticipationInfoRepository; +import com.softeer.backend.fo_domain.draw.service.DrawSettingManager; +import com.softeer.backend.fo_domain.draw.util.DrawResponseGenerateUtil; +import com.softeer.backend.fo_domain.draw.util.DrawUtil; +import com.softeer.backend.fo_domain.share.domain.ShareInfo; +import com.softeer.backend.fo_domain.share.exception.ShareInfoException; +import com.softeer.backend.fo_domain.share.repository.ShareInfoRepository; +import com.softeer.backend.global.common.code.status.ErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DrawDatabaseWithoutLockTest { + private final DrawParticipationInfoRepository drawParticipationInfoRepository; + private final ShareInfoRepository shareInfoRepository; + private final DrawDatabaseWithoutLockUtil drawDatabaseWithoutLockUtil; + private final DrawUtil drawUtil; + private final DrawResponseGenerateUtil drawResponseGenerateUtil; + private final DrawSettingManager drawSettingManager; + + public DrawModalResponseDto participateDrawEvent(Integer userId) { + // 복권 기회 조회 + ShareInfo shareInfo = shareInfoRepository.findShareInfoByUserId(userId) + .orElseThrow(() -> new ShareInfoException(ErrorStatus._NOT_FOUND)); + + // 만약 남은 참여 기회가 0이라면 + if (shareInfo.getRemainDrawCount() == 0) { + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); + } + + drawDatabaseWithoutLockUtil.increaseDrawParticipationCount(); // 추첨 이벤트 참여자수 증가 + shareInfoRepository.decreaseRemainDrawCount(userId); // 횟수 1회 차감 + + // 만약 당첨 목록에 존재한다면 이미 오늘은 한 번 당첨됐다는 뜻이므로 LoseModal 반환 + int ranking = drawDatabaseWithoutLockUtil.getRankingIfWinner(userId); // 당첨 목록에 존재한다면 랭킹 반환 + if (ranking != 0) { + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + + // 당첨자 수 조회 + int first = drawSettingManager.getWinnerNum1(); // 1등 수 + int second = drawSettingManager.getWinnerNum2(); // 2등 수 + int third = drawSettingManager.getWinnerNum3(); // 3등 수 + + // 당첨자 수 설정 + drawUtil.setFirst(first); + drawUtil.setSecond(second); + drawUtil.setThird(third); + + // 추첨 로직 실행 + drawUtil.performDraw(); + + if (drawUtil.isDrawWin()) { // 당첨자일 경우 + ranking = drawUtil.getRanking(); + int winnerNum; + if (ranking == 1) { + winnerNum = first; + } else if (ranking == 2) { + winnerNum = second; + } else { + winnerNum = third; + } + + if (drawDatabaseWithoutLockUtil.isWinner(userId, ranking, winnerNum)) { // 레디스에 추첨 티켓이 남았다면, 레디스 당첨 목록에 추가 + // 추첨 티켓이 다 팔리지 않았다면 + drawParticipationInfoRepository.increaseWinCount(userId); // 당첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawWinnerResponse(ranking); // WinModal 반환 + } else { + // 추첨 티켓이 다 팔렸다면 로직상 당첨자라도 실패 반환 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } else { // 낙첨자일 경우 + drawParticipationInfoRepository.increaseLoseCount(userId); // 낙첨 횟수 증가 + return drawResponseGenerateUtil.generateDrawLoserResponse(userId); // LoseModal 반환 + } + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockUtil.java b/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockUtil.java new file mode 100644 index 00000000..26fe07da --- /dev/null +++ b/src/main/java/com/softeer/backend/fo_domain/draw/test/withoutlock/DrawDatabaseWithoutLockUtil.java @@ -0,0 +1,91 @@ +package com.softeer.backend.fo_domain.draw.test.withoutlock; + +import com.softeer.backend.fo_domain.draw.test.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class DrawDatabaseWithoutLockUtil { + private final DrawTestFirstWinnerListRepository drawTestFirstWinnerListRepository; + private final DrawTestSecondWinnerListRepository drawTestSecondWinnerListRepository; + private final DrawTestThirdWinnerListRepository drawTestThirdWinnerListRepository; + private final DrawTestParticipantCountRepository drawTestParticipantCountRepository; + + public void increaseDrawParticipationCount() { + drawTestParticipantCountRepository.increaseParticipantCount(); + } + + @Transactional + public int getRankingIfWinner(Integer userId) { + if (drawTestFirstWinnerListRepository.existsByUserId(userId)) { + return 1; + } + + if (drawTestSecondWinnerListRepository.existsByUserId(userId)) { + return 2; + } + + if (drawTestThirdWinnerListRepository.existsByUserId(userId)) { + return 3; + } + return 0; + } + + @Transactional(isolation = Isolation.SERIALIZABLE) + public boolean isWinner(Integer userId, int ranking, int winnerNum) { + if (ranking == 1) { + return isFirstWinner(userId, winnerNum); + } + if (ranking == 2) { + return isSecondWinner(userId, winnerNum); + } + if (ranking == 3) { + return isThirdWinner(userId, winnerNum); + } + return false; + } + + @Transactional + public boolean isFirstWinner(Integer userId, int winnerNum) { + if (drawTestFirstWinnerListRepository.count() < winnerNum) { + DrawTestFirstWinnerList firstWinner = DrawTestFirstWinnerList.builder() + .userId(userId) + .build(); + + drawTestFirstWinnerListRepository.save(firstWinner); + return true; + } + return false; + } + + @Transactional + public boolean isSecondWinner(Integer userId, int winnerNum) { + if (drawTestSecondWinnerListRepository.count() < winnerNum) { + DrawTestSecondWinnerList secondWinner = DrawTestSecondWinnerList.builder() + .userId(userId) + .build(); + + drawTestSecondWinnerListRepository.save(secondWinner); + return true; + } + + return false; + } + + @Transactional + public boolean isThirdWinner(Integer userId, int winnerNum) { + if (drawTestThirdWinnerListRepository.count() < winnerNum) { + DrawTestThirdWinnerList thirdWinner = DrawTestThirdWinnerList.builder() + .userId(userId) + .build(); + + drawTestThirdWinnerListRepository.save(thirdWinner); + return true; + } + + return false; + } +} diff --git a/src/main/java/com/softeer/backend/fo_domain/draw/util/DrawUtil.java b/src/main/java/com/softeer/backend/fo_domain/draw/util/DrawUtil.java index fcfe8abb..b4d2b5ee 100644 --- a/src/main/java/com/softeer/backend/fo_domain/draw/util/DrawUtil.java +++ b/src/main/java/com/softeer/backend/fo_domain/draw/util/DrawUtil.java @@ -42,15 +42,15 @@ public class DrawUtil { */ public void performDraw() { Random random = new Random(); - int randomNum = random.nextInt(10000) + 1; // 랜덤 수 + int randomNum = random.nextInt(10) + 1; // 랜덤 수 if (randomNum <= this.first) { isDrawWin = true; ranking = 1; - } else if (randomNum <= this.second) { + } else if (randomNum <= this.first + this.second) { isDrawWin = true; ranking = 2; - } else if (randomNum <= this.third) { + } else if (randomNum <= this.first + this.second + this.third) { isDrawWin = true; ranking = 3; } @@ -104,7 +104,6 @@ public List generateLoseImages() { */ @Cacheable(value = "staticResources", key = "'drawImage_' + #direction") public String getImageUrl(int direction) { - Map s3ContentMap = staticResourceUtil.getS3ContentMap(); String directionImage; diff --git a/src/main/java/com/softeer/backend/fo_domain/user/service/LoginService.java b/src/main/java/com/softeer/backend/fo_domain/user/service/LoginService.java index 260357b4..514bbc84 100644 --- a/src/main/java/com/softeer/backend/fo_domain/user/service/LoginService.java +++ b/src/main/java/com/softeer/backend/fo_domain/user/service/LoginService.java @@ -110,7 +110,7 @@ private void createShareInfo(Integer userId) { ShareInfo shareInfo = ShareInfo.builder() .userId(userId) .invitedNum(0) - .remainDrawCount(1) + .remainDrawCount(1000) .build(); shareInfoRepository.save(shareInfo); diff --git a/src/main/resources/check_winner.lua b/src/main/resources/check_winner.lua new file mode 100644 index 00000000..5132067f --- /dev/null +++ b/src/main/resources/check_winner.lua @@ -0,0 +1,12 @@ +-- Lua Script: 특정 SET에서 사용자 ID가 존재하는지 확인 +local draw_winner_key = KEYS[1] +local user_id = ARGV[1] -- 사용자 ID를 문자열로 변환 + +-- SET에서 user_id가 존재하는지 확인 +local exists = redis.call('SISMEMBER', draw_winner_key, user_id) + +if exists == 1 then + return 1 +else + return 0 +end diff --git a/src/test/java/com/softeer/backend/fo_domain/draw/service/DrawServiceTest.java b/src/test/java/com/softeer/backend/fo_domain/draw/service/DrawServiceTest.java index c03b8095..dc4029bd 100644 --- a/src/test/java/com/softeer/backend/fo_domain/draw/service/DrawServiceTest.java +++ b/src/test/java/com/softeer/backend/fo_domain/draw/service/DrawServiceTest.java @@ -147,17 +147,17 @@ void getDrawMainPageFullAttend() { lenient().when(drawAttendanceCountUtil.handleAttendanceCount(userId, drawParticipationInfo)).thenReturn(7); - when(drawRemainDrawCountUtil.handleRemainDrawCount(userId, 1, drawParticipationInfo)) - .thenReturn(3); + lenient().when(drawRemainDrawCountUtil.handleRemainDrawCount(userId, 1, drawParticipationInfo)) + .thenReturn(1); DrawMainFullAttendResponseDto expectedResponse = DrawMainFullAttendResponseDto.builder() .invitedNum(3) - .remainDrawCount(3) + .remainDrawCount(1) .drawAttendanceCount(7) .fullAttendModal(fullAttendModal) .build(); - when(drawResponseGenerateUtil.generateMainFullAttendResponse(3, 3, 7 % 8)) + lenient().when(drawResponseGenerateUtil.generateMainFullAttendResponse(3, 3, 7 % 8)) .thenReturn(expectedResponse); // when @@ -204,7 +204,7 @@ void getDrawMainPageNotAttend() { when(drawAttendanceCountUtil.handleAttendanceCount(userId, drawParticipationInfo)).thenReturn(1); - when(drawRemainDrawCountUtil.handleRemainDrawCount(userId, shareInfo.getRemainDrawCount(), drawParticipationInfo)) + lenient().when(drawRemainDrawCountUtil.handleRemainDrawCount(userId, shareInfo.getRemainDrawCount(), drawParticipationInfo)) .thenReturn(1); DrawMainResponseDto expectedResponse = DrawMainResponseDto.builder()