Compare commits

..

2 Commits

Author SHA1 Message Date
Klaus e0d48712ac 콘텐츠 예약 오픈 설정
- 콘텐츠 id뿐 아니라 콘텐츠 전체를 불러와서 중복호출 하지 않도록 수정
2024-12-02 08:25:55 +09:00
Klaus 05592f94b9 스프링 스케줄러를 이용하여 콘텐츠 예약 오픈 설정 2024-12-02 08:22:16 +09:00
7 changed files with 85 additions and 11 deletions

View File

@ -26,6 +26,7 @@ repositories {
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-security")
@ -33,6 +34,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.retry:spring-retry")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.redisson:redisson-spring-boot-starter:3.17.7")
// jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.5")

View File

@ -4,8 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.retry.annotation.EnableRetry
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableRetry
class SodaLiveApplication

View File

@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.common.annotation
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.stereotype.Component
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class SchedulerOnly
@Aspect
@Component
class SchedulerOnlyAspect {
@Before("@annotation(SchedulerOnly)")
fun checkSchedulerAccess() {
if (!isSchedulerThread()) {
throw IllegalStateException("잘못된 접근입니다.")
}
}
private fun isSchedulerThread(): Boolean {
// 스케줄러 스레드 여부를 판단하는 간단한 로직
return Thread.currentThread().name.contains("scheduler")
}
}

View File

@ -1,5 +1,8 @@
package kr.co.vividnext.sodalive.configs
import org.redisson.Redisson
import org.redisson.api.RedissonClient
import org.redisson.config.Config
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
@ -26,6 +29,17 @@ class RedisConfig(
@Value("\${spring.redis.port}")
private val port: Int
) {
@Bean
fun redissonClient(): RedissonClient {
val config = Config()
config.useSingleServer()
.setAddress("redis://$host:$port")
.setConnectionMinimumIdleSize(1) // 최소 유휴 연결: 1
.setConnectionPoolSize(5) // 최대 연결 풀 크기: 5
return Redisson.create(config)
}
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
val clientConfiguration = LettuceClientConfiguration.builder()

View File

@ -120,7 +120,7 @@ interface AudioContentQueryRepository {
fun getAudioContentCurationList(isAdult: Boolean, offset: Long, limit: Long): List<AudioContentCuration>
fun getNotReleaseContentId(): List<Long>
fun getNotReleaseContent(): List<AudioContent>
fun isContentCreator(contentId: Long, memberId: Long): Boolean
@ -762,15 +762,14 @@ class AudioContentQueryRepositoryImpl(
.fetch()
}
override fun getNotReleaseContentId(): List<Long> {
override fun getNotReleaseContent(): List<AudioContent> {
val where = audioContent.isActive.isFalse
.and(audioContent.releaseDate.isNotNull)
.and(audioContent.releaseDate.loe(LocalDateTime.now()))
.and(audioContent.duration.isNotNull)
return queryFactory
.select(audioContent.id)
.from(audioContent)
.selectFrom(audioContent)
.where(where)
.fetch()
}

View File

@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.common.annotation.SchedulerOnly
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag
import kr.co.vividnext.sodalive.content.hashtag.HashTag
@ -402,14 +403,12 @@ class AudioContentService(
}
}
@SchedulerOnly
@Transactional
fun releaseContent() {
val contentIdList = repository.getNotReleaseContentId()
for (contentId in contentIdList) {
val audioContent = repository.findByIdOrNull(contentId)
?: throw SodaException("잘못된 요청입니다.")
val notReleasedAudioContent = repository.getNotReleaseContent()
for (audioContent in notReleasedAudioContent) {
audioContent.isActive = true
applicationEventPublisher.publishEvent(
@ -418,7 +417,7 @@ class AudioContentService(
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = contentId,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
container = "ios"
)
@ -430,7 +429,7 @@ class AudioContentService(
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = contentId,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
container = "aos"
)

View File

@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.scheduler
import kr.co.vividnext.sodalive.content.AudioContentService
import org.redisson.api.RedissonClient
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
@Service
class AudioContentReleaseSchedulerService(
private val redissonClient: RedissonClient,
private val audioContentService: AudioContentService
) {
@Scheduled(fixedRate = 1000 * 60 * 5)
fun release() {
val lock = redissonClient.getLock("lock:audioContentRelease")
if (lock.tryLock(10, TimeUnit.SECONDS)) {
try {
println("락을 획득하여 배포를 시작합니다.")
audioContentService.releaseContent()
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
println("락 해제")
}
}
} else {
println("락을 획득하지 못해서 배포를 건너뜁니다")
}
}
}