Redis를 이용한 동시성 문제 해결
개요
동시성 문제를 해결할 수 있는 방법은 여러가지가 있다. Java의 synchronized
키워드, Optimistic Lock
, Pessimistic Lock
등이 있다. 하지만 이미 여러 서버 node들이 존재하고 이미 한대 이상의 Redis 노드가 있다면 Redisson Lock
을 사용하는게 대부분 속도, 성능을 고려했을때 최적의 선택지다.
그래서 오늘은 Redisson Lock을 활용해서 동시성 문제를 해결해보겠다.
라이브러리 주입
build.gradle
1
2
3
4
dependencies {
implementation 'org.redisson:redisson-spring-boot-starter'
// ...
}
Redis 관련 설정
RedisConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
private static final String REDISSON_HOST_PREFIX = "redis:://";
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
Redisson Lock 애노테이션
RedissonLock.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {
/**
* Lock 이름 (고유값)
*/
String key();
/**
* Lock 시간 단위
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* Lock 획득을 시도하는 최대 시간
*/
long waitTime() default 5000L;
/**
* Lock 획득 후 점유하는 최대 시간
*/
long leaseTime() default 2000L;
}
RedissonLockAspect.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RedissonLockAspect {
private final RedissonClient redissonClient;
@Around("@annotation(com.tripbtoz.account.annotation.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
String key = getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), annotation.key());
RLock lock = redissonClient.getLock(key);
log.debug("Attempting to lock method: {} with key: {}", method, key);
boolean lockAcquired = false;
try {
lockAcquired = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), annotation.timeUnit());
if (!lockAcquired) {
log.debug("Failed to acquire lock for key: {}", key);
return false;
}
return joinPoint.proceed();
} catch (InterruptedException e) {
log.error("Interrupted while trying to lock method: {}, errorMessage: {}", method, e.getMessage());
Thread.currentThread().interrupt();
throw e;
} catch (Exception e) {
log.error("Exception in method: {}, errorMessage: {}", method, e.getMessage(), e);
throw e;
} finally {
if (lockAcquired) {
try {
lock.unlock();
log.debug("Unlocked key: {}", key);
} catch (Exception e) {
log.error("Failed to unlock key: {}", key, e);
}
}
}
}
private String getDynamicValue(String[] parameterNames, Object[] args, String key) {
SpelExpressionParser 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, String.class);
}
}
Lock 메서드에 적용
SomeService.java
1
2
3
4
5
6
7
8
public class SomeService {
// ...
@RedissonLock(key = "'function_' + #request.id")
public void function(Request request) {
// ...
}
}
테스트 코드
SomeServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SomeServiceTest {
// ...
@DisplayName("동시성 문제가 발생할시 정상 작동한다")
@Test
void givenConcurrent_whenFunction_thenReturn() throws InterruptedException {
// given
Request request = createValidRequest();
int threadCnt = 3;
CountDownLatch doneSignal = new CountDownLatch(threadCnt);
ExecutorService executorService = Executors.newFixedThreadPool(threadCnt);
AtomicInteger successCnt = new AtomicInteger();
AtomicInteger failCnt = new AtomicInteger();
// when
for (int i = 0; i < threadCnt; i++) {
executorService.execute(() -> {
try {
someService.function(request);
successCnt.getAndIncrement();
} catch (Exception e) {
failCnt.getAndIncrement();
} finally {
doneSignal.countDown();
}
});
doneSignal.await();
executorService.shutdown();
}
// then
assertAll(
() -> assertThat(successCnt.get()).isEqualTo(1),
() -> assertThat(failCnt.get()).isEqualTo(2)
);
}
}
참조
- https://helloworld.kurly.com/blog/distributed-redisson-lock/
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.