-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat: 동시성 제어 어노테이션 기능 구현 #876
base: develop
Are you sure you want to change the base?
Changes from all commits
efb5253
46066c4
d042f04
c730a9f
e4c286c
ee4d930
acb5401
9c3b8a9
92da359
2b45e71
0b23205
88b57cb
ca3495e
febfb9b
783423b
e319625
88c738a
4ed9a21
d1da667
ff4ac9d
8ac8017
75790d6
7340a63
1b506a1
2bec31e
edc8606
b635a9b
cf493f6
6de9b2c
cca6571
689743b
e544c53
7f66618
8de60b8
a15553b
f46e065
54a141a
9717a0c
9979b6c
5386b9b
46585a9
88294fb
e7903e0
398b1c3
e487a29
3ebc1cf
3bffd87
8300506
3206f1e
2b2c90a
98701b8
f0a8a03
005aac4
97eb831
ef24f8a
8fd190e
9d9117a
da1606b
c58d3ae
9afe353
ca24540
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package in.koreatech.koin.global.concurrent; | ||
|
||
import static java.lang.annotation.ElementType.METHOD; | ||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.springframework.context.annotation.Profile; | ||
|
||
@Documented | ||
@Target(METHOD) | ||
@Retention(RUNTIME) | ||
@Profile("!test") | ||
public @interface ConcurrencyGuard { | ||
|
||
String lockName(); | ||
|
||
long waitTime() default 5L; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A궁금한 점인데, 기본적으로 5초, 3초로 하면 조금 시간이 길지 않나요? |
||
|
||
long leaseTime() default 3L; | ||
|
||
TimeUnit timeUnit() default TimeUnit.SECONDS; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package in.koreatech.koin.global.concurrent; | ||
|
||
import java.lang.reflect.Method; | ||
|
||
import org.aspectj.lang.ProceedingJoinPoint; | ||
import org.aspectj.lang.annotation.Around; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.aspectj.lang.reflect.MethodSignature; | ||
import org.redisson.api.RLock; | ||
import org.redisson.api.RedissonClient; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
|
||
import in.koreatech.koin.global.concurrent.exception.ConcurrencyLockException; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@Aspect | ||
@Component | ||
@Profile("!test") | ||
@RequiredArgsConstructor | ||
public class ConcurrencyGuardAspect { | ||
|
||
private final RedissonClient redissonClient; | ||
private final TransactionAspect transactionAspect; | ||
|
||
@Around("@annotation(ConcurrencyGuard) && (args(..))") | ||
public Object handleConcurrency(ProceedingJoinPoint joinPoint) throws Throwable { | ||
ConcurrencyGuard annotation = getAnnotation(joinPoint); | ||
|
||
Object[] args = joinPoint.getArgs(); | ||
|
||
String lockName = getLockName(args, annotation); | ||
RLock lock = redissonClient.getLock(lockName); | ||
|
||
try { | ||
boolean available = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), annotation.timeUnit()); | ||
|
||
if (!available) { | ||
throw ConcurrencyLockException.withDetail("Redisson GetLock 타임 아웃 lockName: " + lockName); | ||
} | ||
|
||
return transactionAspect.proceed(joinPoint); | ||
} finally { | ||
try { | ||
lock.unlock(); | ||
} catch (IllegalMonitorStateException e) { | ||
log.warn("Redisson 락이 이미 해제되었습니다 lockName: " + lockName); | ||
} | ||
} | ||
} | ||
|
||
private ConcurrencyGuard getAnnotation(ProceedingJoinPoint joinPoint) { | ||
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); | ||
Method method = signature.getMethod(); | ||
return method.getAnnotation(ConcurrencyGuard.class); | ||
} | ||
|
||
private String getLockName(Object[] args, ConcurrencyGuard annotation) { | ||
String lockNameFormat = "lock:%s:%s"; | ||
|
||
String relevantParameter; | ||
if (args.length > 0) { | ||
relevantParameter = args[0].toString(); | ||
} else { | ||
relevantParameter = "default"; | ||
} | ||
|
||
return String.format(lockNameFormat, annotation.lockName(), relevantParameter); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package in.koreatech.koin.global.concurrent; | ||
|
||
import org.aspectj.lang.ProceedingJoinPoint; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.transaction.annotation.Propagation; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Profile("!test") | ||
@Component | ||
public class TransactionAspect { | ||
// leaseTime보다 트랜잭션 타임아웃은 작아야 한다. | ||
// leastTimeOut 발생 전에 rollback 시키기 위함 | ||
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A트랜잭션에 시간도 걸 수 있었군요 👀 |
||
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable { | ||
return joinPoint.proceed(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package in.koreatech.koin.global.concurrent.exception; | ||
|
||
import in.koreatech.koin.global.auth.exception.AuthorizationException; | ||
import in.koreatech.koin.global.exception.KoinException; | ||
|
||
public class ConcurrencyLockException extends KoinException { | ||
|
||
private static final String DEFAULT_MESSAGE = "현재 요청을 처리할 수 없습니다. 잠시 후 다시 시도해 주세요."; | ||
|
||
public ConcurrencyLockException(String message) { | ||
super(message); | ||
} | ||
|
||
public ConcurrencyLockException(String message, String detail) { | ||
super(message, detail); | ||
} | ||
|
||
public static ConcurrencyLockException withDetail(String detail) { | ||
return new ConcurrencyLockException(DEFAULT_MESSAGE, detail); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package in.koreatech.koin.global.config; | ||
|
||
import org.redisson.Redisson; | ||
import org.redisson.api.RedissonClient; | ||
import org.redisson.config.Config; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Profile; | ||
|
||
@Configuration | ||
@Profile("!test") | ||
public class RedissonConfig { | ||
|
||
@Value("${spring.data.redis.host}") | ||
private String redisHost; | ||
|
||
@Value("${spring.data.redis.port}") | ||
private int redisPort; | ||
|
||
@Value("${spring.data.redis.password:}") | ||
private String redisPassword; | ||
|
||
private static final String REDISSION_HOST_PREFIX = "redis://"; | ||
|
||
@Bean | ||
public RedissonClient redissionClient() { | ||
Config config = new Config(); | ||
config.useSingleServer() | ||
.setAddress(REDISSION_HOST_PREFIX + redisHost + ":" + redisPort) | ||
.setPassword(redisPassword.isEmpty() ? null : redisPassword); | ||
return Redisson.create(config); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
ALTER TABLE `article_search_keywords` | ||
MODIFY COLUMN `keyword` VARCHAR(255) NOT NULL UNIQUE; | ||
|
||
ALTER TABLE article_search_keyword_ip_map | ||
ADD CONSTRAINT unique_keyword_ip UNIQUE (keyword_id, ip_address), | ||
ADD CONSTRAINT fk_keyword_id | ||
FOREIGN KEY (keyword_id) | ||
REFERENCES article_search_keywords (id) | ||
ON DELETE CASCADE; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C마지막 공백 라인 지워주세요~!! + 리뷰 후 머지 전 flyway 번호 수정!!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A
그럼 이건 userId를 키값으로 동일한 userId에서 Lock이 걸리는건가요?
그리고 현재 메서드에서 4개의 요청이 동시에 들어온 상황에 대해 궁금한 점이 있습니다. 예를들어 4개의 API 요청이 동시에 들어온다면 1개의 요청은 바로 DB에 쿼리문으로 보내지고, 1개의 요청은 3초뒤에 DB에 쿼리문으로 보내지며, 2개의 요청은 3초동안 대기하다가 에러가 발생되는건가요??
만약 위와같은 방식이라면 사용자가 frame을 동시에 여러개 지우려할때 3초동안 아무런 에러메세지 반응도 없어서 불편함을 겪지 않을까요? 제가 해당 기술에 대해 지식이 있지않다보니 약간 헷갈리네요...