Skip to content

Commit

Permalink
refactor: redisson distributed lock aop
Browse files Browse the repository at this point in the history
  • Loading branch information
alstn113 committed Dec 17, 2024
1 parent 820416d commit e73349e
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 134 deletions.
2 changes: 1 addition & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.redisson:redisson-spring-boot-starter:3.33.0'

// lombok
compileOnly 'org.projectlombok:lombok'
Expand Down
21 changes: 0 additions & 21 deletions server/src/main/java/com/fluffy/global/cache/LockManager.java

This file was deleted.

30 changes: 0 additions & 30 deletions server/src/main/java/com/fluffy/global/cache/RedisConfig.java

This file was deleted.

51 changes: 0 additions & 51 deletions server/src/main/java/com/fluffy/global/cache/RedisLockUtil.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.fluffy.global.redis;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class AopForTransaction {

@Transactional(propagation = Propagation.REQUIRES_NEW)
public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.fluffy.global.redis;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class CustomSpringELParser {

private CustomSpringELParser() {
}

public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}

return parser.parseExpression(key).getValue(context, Object.class);
}
}
26 changes: 26 additions & 0 deletions server/src/main/java/com/fluffy/global/redis/DistributedLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.fluffy.global.redis;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

String key();

TimeUnit timeUnit() default TimeUnit.SECONDS;

/**
* 락을 획득하기 위해 대기할 시간
*/
long waitTime() default 3L;

/**
* 락을 유지할 시간
*/
long leaseTime() default 3L;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.fluffy.global.redis;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {

private static final String LOCK_PREFIX = "lock:";

private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;

@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String key =
LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(),
distributedLock.key());

RLock rLock = redissonClient.getLock(key);

try {
boolean isLocked = rLock.tryLock(
distributedLock.waitTime(),
distributedLock.leaseTime(),
distributedLock.timeUnit()
);

if (!isLocked) {
return false;
}

return aopForTransaction.proceed(joinPoint);
} finally {
try {
rLock.unlock();
} catch (Exception e) {
log.info("[DistributedLock] Failed to release lock for key: {}", key, e);
}
}
}
}
32 changes: 32 additions & 0 deletions server/src/main/java/com/fluffy/global/redis/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.fluffy.global.redis;

import lombok.RequiredArgsConstructor;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
@RequiredArgsConstructor
public class RedisConfig {

private final RedisProperties properties;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(properties.host(), properties.port());
}

@Bean
public RedissonClient redissonClient() {
RedissonClient redisson;
Config config = new Config();
config.useSingleServer().setAddress("redis://%s:%d".formatted(properties.host(), properties.port()));
redisson = Redisson.create(config);

return redisson;
}
}
13 changes: 13 additions & 0 deletions server/src/main/java/com/fluffy/global/redis/RedisProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.fluffy.global.redis;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.data.redis")
public record RedisProperties(
@NotBlank String host,
@NotNull @Positive int port
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import com.fluffy.auth.domain.MemberRepository;
import com.fluffy.exam.domain.Exam;
import com.fluffy.exam.domain.ExamRepository;
import com.fluffy.global.cache.RedisLockUtil;
import com.fluffy.global.exception.BadRequestException;
import com.fluffy.global.redis.DistributedLock;
import com.fluffy.submission.application.dto.SubmissionAppRequest;
import com.fluffy.submission.domain.Submission;
import com.fluffy.submission.domain.SubmissionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand All @@ -21,10 +20,9 @@ public class SubmissionService {
private final SubmissionRepository submissionRepository;
private final MemberRepository memberRepository;
private final SubmissionMapper submissionMapper;
private final RedisLockUtil redisLockUtil;

@Transactional
public void submit(SubmissionAppRequest request) {
@DistributedLock(key = "#lockName")
public void submit(SubmissionAppRequest request, String lockName) {
Member member = memberRepository.getById(request.accessor().id());
Exam exam = examRepository.getById(request.examId());

Expand All @@ -36,11 +34,7 @@ public void submit(SubmissionAppRequest request) {
throw new BadRequestException("이미 제출한 시험입니다.");
}

String key = "submission:%d:%d".formatted(exam.getId(), member.getId());
redisLockUtil.acquireAndRunLock(key, () -> {
Submission submission = submissionMapper.toSubmission(exam, member.getId(), request);
submissionRepository.save(submission);
return null;
});
Submission submission = submissionMapper.toSubmission(exam, member.getId(), request);
submissionRepository.save(submission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public ResponseEntity<Void> submit(
@RequestBody @Valid SubmissionWebRequest request,
@Auth Accessor accessor
) {
submissionService.submit(request.toAppRequest(examId, accessor));
String lockName = "submit:%d:%d".formatted(examId, accessor.id());
submissionService.submit(request.toAppRequest(examId, accessor), lockName);

return ResponseEntity.ok().build();
}
Expand Down
2 changes: 0 additions & 2 deletions server/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ spring:
redis:
host: localhost
port: 6379
cache:
type: redis
mail:
host: ${MAIL_HOST}
port: 587
Expand Down
2 changes: 0 additions & 2 deletions server/src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ spring:
redis:
host: fluffy-redis
port: 6379
cache:
type: redis
mail:
host: ${MAIL_HOST}
port: 587
Expand Down
Loading

0 comments on commit e73349e

Please sign in to comment.