포스트

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 라이센스를 따릅니다.