본문 바로가기

Language & Framework/Spring

Spring 동시성 문제 때려잡기 (3) Redis lock (Lettuce, Redisson)

 

https://7357.tistory.com/338

 

Spring 동시성 문제 때려잡기 (1) DB Lock

현재 진행 중인 프로젝트는 시간 대여 서비스로, 누군가가 주최한 모임의 정해진 시간에 참여를 신청하고 예약/결제하는 그런 로직을 가지고 있다. 근데 처음 어플리케이션을 설계하는 단계에

7357.tistory.com

https://7357.tistory.com/339

 

Spring 동시성 문제 때려잡기 (2) Application Lock (synchronized, semaphore, ReentrantLock)

1.이 글은 지난편 "Spring 동시성 문제 때려잡기(1) DB Lock"에서 이어지는 글로 전제조건에 대해서 다시 설명하지 않습니다. 2. 여러 문서를 참고하여 해당 포스팅에 미처 포함하지 못한 다양한 테스

7357.tistory.com

 

1편에서는 DB Lock에 대해 알아보았으며 2편에서는 Application Lock에 대해 알아보았다.

Application Lock이 DB Lock보다 더 유연하며, 일반적으로 더 빠르다고 여겨지지만 분산환경에서 사용할 수 없다는 단점이 있다.

반면, DB Lock은 분산 환경에서도 사용 가능하지만 DB Connection이라는 자원을 활용해야 한다.

결국 이는 DB의 Availability를 저하시키기 때문에 대안을 생각해볼 필요성이 있다.

(물론 인프라의 여유가 있을 때, 그리고 그 정도의 커넥션 비용 절감이 필요할 때의 이야기다.)

 

오늘은 그 중 Redis를 이용해서 동시성 문제를 해결하는 방법을 다루고자 한다.

이번 글이 마지막이 될지, Kafka를 이용한 동시성 문제 해결에 대해서도 다루게 될지는 잘 모르겠다.

 

우선 Redis를 이용할 때 택할 수 있는 방법은 내장 redis clinet인 Lettuce를 이용하는 것과, 외부 라이브러리인 Redisson을 사용하는 것 두 가지로 나뉠 수 있다.

 

물론 Jedis로도 가능하겠지만, 외장 라이브러리를 설치해가면서 Jedis를 이용할 이유는 없다.

기본적인 성능 자체가 Lettuce가 더 좋으며, Jedis 자체가 Thread-Safe하지 않기 때문에 별도의 처리가 필요하다.

(Jedis의 Thread-Safe와 Redis의 Thread-Safe는 별개의 문제다.)

 

성능 관련한 내용은 아래 동욱님의 블로그를 참고하자.

 

https://jojoldu.tistory.com/418

 

Jedis 보다 Lettuce 를 쓰자

Java의 Redis Client는 크게 2가지가 있습니다. Jedis Lettuce 둘 모두 몇천개의 Star를 가질만큼 유명한 오픈소스입니다. 이번 시간에는 둘 중 어떤것을 사용해야할지에 대해 성능 테스트 결과를 공유하

jojoldu.tistory.com

 

 

 

첫 번째로 내장 클라이언트인 Lettuce로 분산 락을 구현 할 수 있다.

이는 1편에서 다루었던 User Level Lock과 겉보기에 비슷한 방법으로 동작한다.

 

어떤 Key 값 자체를 한 개만 저장할 수 있도록 해놓고, 해당 Key가 이미 Redis에 존재한다면 Key가 삭제되기 전까지 다른 쓰레드는 ciritical section에 진입할 수 없게 함으로써 race condition을 방지하는 것이다.

 

 

 

우선 이름은 상관 없고, 레디스 스토어를 원하는 이름으로 만든 다음에 lock과 unlock 메서드를 구현한다.

lock 메서드에서는 키를 생성하고, unlock 메서드에서는 해당 키를 삭제하게 구현하면 끝이다.

 

 

 

setIfAbsent는 메서드 이름에서 알 수 있듯이, 해당 키가 없다면 추가하고 존재한다면 일정 시간 대기하다가 실패할 경우 false를 반환한다.

키를 추가하는 것에 성공했다면 true를 반환한다.

 

 

 

 

이를 이용해 lock을 구현한다면 이런 모습이 된다.

위는 만약 락 획득에 실패했을 때 재요청이 필요한 경우이다.

 

전형적인 busy waiting

 

이는 busy waiting의 일종인 spin lock 방식을 활용하게 된다.

busy waiting이 무조건적으로 나쁜 것은 아니며, wait(), signal() 방식보다 오버헤드가 적어 속도가 빠르다는 장점이 있다.

그러나 대부분의 시스템에서 극도로 미세한 차이의 속도 향상을 위해 busy waiting을 사용하는 것은 지양하고 있다는 것을 감안했을 때 보다 나은 방법을 생각하지 않을 수 없다.

 

이에 두가지 해결책이 있다.

1. Lettuce에서 직접 Pub-Sub 구조를 구현한다.

: 절대적으로 외부 라이브러리 의존성을 줄이고 싶다면 해당 방법을 택할 수도 있을 것이다.

2. Redisson 라이브러리에서 제공하는 직관적인 메서드를 활용한다.

: 외부 라이브러리 의존을 크게 신경쓰지 않는다면, 분명히 쉬운 길이 될 것이다.

 

1번은 가능한 게 맞는지 확실하지 않다.

Lettuce에서도 RedisConnectionFactory를 주입받은 뒤, Connection을 얻고, 이후 메서드를 통해 pub/sub 구조를 구현할 수 있다.

또한 이걸 이용해 락을 구현하는 것도 가능하며, 내가 구현한 방식으로 테스트 코드를 통과하였으나 구글에 Lettuce를 사용한 pub/sub 락에 대해 검색해봐도 마땅한 정보를 찾을 수 없다.

내가 할 수 있는데 다른 사람이 아무도 쓰지 않는다는 게 신뢰가 별로 가지 않아서 여기에 코드를 올리지는 않겠다.

직접 구현해보고 싶다면 Lettuce Pub/Sub이라고 구글에 검색해보자.

 

2번의 경우 아주 간단하다.

그냥 제공해주는 메서드를 쓰면 끝난다.

 

implementation 'org.redisson:redisson-spring-boot-starter:3.19.1'

 

우선, Gradle에 redisson에 대한 dependency를 추가해주자.

 

 

 

Redisson에 대한 설정을 추가한다.

Lettuce랑 조금 다른 부분이 있어서 당황스러울 수 있다.

 

 

 

 

그리고 구현해주면 끝.

언뜻 보면 위의 spin lock 방식과 별 차이가 없어보일 수 있으나, 이는 최대 대기시간을 설정하고 문제가 생겼을 때 메서드를 재호출해주기 위해 try, catch 처리를 해준 것일 뿐 대기하는 시간동안 쓰레드는 불필요한 재요청을 하지 않고 redis가 신호를 주기를 기다린다.

 

 

구독자는 메세지가 올 때까지 대기하거나, 최대 대기 시간이 지나면 다시 요청한다.

 

 

 

 

tryLock 메서드는 waitTime과 leaseTime이라는 파라미터를 제공한다.

waitTime은 쓰레드가 잠들지 않고 기다리는 시간이고, leaseTime은 lock을 획득한 쓰레드가 lock을 쥐고 있을 수 있는 시간을 의미한다.

 

우리가 지겹도록 외운 데드락의 조건을 생각해보자.

상호배제, 비선점, 순환대기, 점유대기를 모두 만족해야 데드락이 발생하는데, Redisson은 leaseTime이라는 파라미터로 이를 해결한다.

 

근데 사실 내가 구현한 코드 자체가 데드락의 가능성을 가지고 있다.

데드락을 막고 싶다면 대기 후에도 락 획득 실패 시 그냥 요청을 실패시키는 쪽이 더 안전할 것이다.

이유는 코드를 다시 읽으면서 생각해보자.

 

 

 

 

근데 마무리를 어떻게 해야하지?

아무튼 오늘은 여기서 끝이다.

 

카프카를 활용한 동기화는 예시로 올리려면 터미널도 필요할 것 같고 어플리케이션을 여러개 사용해야해서 글로 쓰면 너무 길어질 것 같아서 귀찮다.

그래도 시간이 남으면 올려보고 싶다.

끝.