🎆 들어가기 앞서
이전 블로그에서 모놀리스 방식에서 Sync와 분산락을 사용해서 동시성을 제어하는 것을 보여준적이 있다.
https://latewalk.tistory.com/244
🚢 재고 관리를 위한 동시성 제어 (Monolithic Architecture)
🦐 모놀리식 상황에서 재고 처리 동시성 문제먼저 동시성을 해결해야 하는 문제가 발생했다 바로 주문을 할때 여러명이 동시에 같은 물건을 주문한 상황이 발생한 것이다. 아 물론 발생한건
latewalk.tistory.com
하지만 이제 서비스가 MSA 방식으로 변경되면서 수정 사항이 생겼다
Sync
동기화 방식을 사용하는 Synchronized는 더이상 사용할 수 없다 왜냐하면 MSA의 특징 상 서비스는 scale-out하게 되면서 동일한 서비스가 늘어난다. 서비스가 여러개로 늘어나면 각 서비스에 들어온 쓰레드를 동기화하는 것이지 실제로 DB에 접근하는 쓰레드는 서비스의 갯수만큼 동시에 접근한다. 그렇게 문제가 발생하게 되는 것이다.
Pessimistic Lock(비관적 락)
비관적 락은 DB 자체에 락을 건다는 사실을 알고있다. 하지만 비관적 락은 요청이 많으면 많을수록 굉장히 심각한 성능의 부하를 일으킬수 있다. 왜냐하면 DB의 데이터 접근 조차도 막기 때문에 해당 쓰레드가 일을 마칠때까지 그 뒤의 모든 쓰레드는 놀고 있어야 하기 때문이다.
🎆 Redis
비관적 락을 아직 사용할 수 있지만 비관적 락은 이전의 내용에서 다루어보았기 때문에 이번엔 조금 새로운 방법 바로 분산락(Spin Lock)을 사용해보고자 한다. 분산락은 Redis, MySQL 에서 구현을 할 수 있지만 그중 가장 유명한 방법인 redis를 사용해보고자 한다.
Spin Lock
Redis Client 인 Redisson, Lettuce는 스핀락을 적절한 주기로 적당량을 보내면서, 서버에 가는 부하를 줄이는 방식이다
서버 측에서 구독한 클라이언트에게 "락을 사용해도 된다" 라고 알림을 주고, 락의 획득 여부를 클라이언트가 계속해서 요청하지 않아도 된다.
하지만 스핀락 또한 비관적 락처럼 비슷하게 서버에 굉장히 많은 부하를 준다.
1. Redis의 Lettuce & SETNX
분산락을 해결하기 위해서 Lettuce를 활용한다. 락을 획득하는 연산이 atomic하게 이루어져 있다. Redis는 값이 존재하지 않으면 셋팅한다 라는 SETNX 명령어를 지원한다. 이 SETNX를 이용해서 Redis의 값이 존재하지 않으면 세팅하게 하고 값이 세팅 되었는지 여부를 리턴값으로 받아 락을 획득하는데 성공한다
♭ 구현
구현을 하는 자체는 그리 어렵지 않다. 다만 주의해야 할 것은 Redis에서 SETNX를 사용하는 방법과 락을 얻어오는 방법을 주의하면 끝이다.
※ 참고로 Spin Lock 을 사용하는 서비스 로직은 Transactional을 사용할 수 없다.
- setIfAbsent : SETNX를 구현하기 위한 값이다. 위에서 설정값은 key를 설정하고 lock이라는 이름을 저장하고 만료시간을 3초로 지정한다. 만약 서비스 로직 처리 시간이 3초 이상이면 더 길게 설정해도 된다
※ NX : 해당 key로 값을 저장하려는데 존재한다면 입력하지 않는다. - 서비스 단에서 우선 lock을 가져올 수 있는지 while을 통해 계속해서 확인한다
- 만약 lock을 획득했다면 서비스 로직을 수행하고 finally 로 lock을 반환하고 로직을 종료한다.
♭ 문제점
- Redis에 너무 많은 부하가 발생
- 위의 코드를 보면 while을 통해 Sprin lock 점유 시도를 무한하게 반복하고 이것은 모두 Redis에게 요청을 한다는 것을 뜻한다. 그리고 부하를 줄이기 위해 while문 내부의 sleep의 시간을 늘린다면 결국 그만큼의 시간이 늘어나기 때문에 성능이 안좋다는 건 변함없다
- Lock의 Timeout이 없다
- 한 Thread 가 Lock을 획득 한 후 오류로 인해 UnLock에 실패한다면, 다른 쓰레드가 Lock을 획득하지 못하여 무한 루프에 빠지게 된다. 때문에 Lock 획득 시도 횟수를 지정해주는 방법으로 이를 예방하는 로직도 추가로 도입해야한다
2. Redis의 Redisson
위에서 사용한 Lettuce 방식은 직접 명령어를 제공했지만 그와 다르게 Bucket, Map 과 같은 자료구조나 Lock과 같은 특정한 구현체의 형태로 Pub/Sub 방식을 사용한다
Pub/Sub 이란?
사이에 메세지 큐 방식처럼 가운데에 다양한 Channel을 두고 특정 Channel을 Subscribe해둔 Subscriber가 해당 Channel에 값이 들어오면 해당 내용을 사용한다.
별도의 Redisson 라이브러리 의존성을 추가해야 하고 사용법 또한 학습해야 한다는 단점이 존재한다.
♭ 구현
Redisson은 위에서 설명했듯이 Pub/Sub 방식을 사용해서 레디스에 발생하는 트래픽을 크게 줄엿다.
락이 해제 될 때마다 Subcribe하는 클라이언트에게 "이제 락을 획득을 시도하여도 된다" 라는 알림을 전달하고 일일이 레디스에 요청을 보내 락의 획득 가능 여부를 체크하지 않아도 되도록 개선되었다
이로써 수많은 요청이 계속해서 레디스에 요청을 수없이 하는 것을 막을 수 있다.
메인 코드 ( tryLock )
"public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException"
tryLock() 메소드는 Redisson에서 Lock을 점유하는 시도를 수행하는 메소드이다.
● waitTime : 락 획득을 대기하는 시간
● leaseTime : 락을 점유 할 수 있는 최대 시간
● unit : 각 시간의 단위를 나타내는 파라미터
- tryLock의 진행 방식
- waitTime 시간 동안 Lock 획득을 시도하므로 재시도를 위한 로직을 따로 작성할 필요가 없다
- leaseTime 시간 이후 자동으로 unlock이 수행되기 때문에 뒤에 있는 쓰레드가 무한 대기에 걸리지 않는다
- Redis에 Key를 기반으로 락을 생성하고 다른 클라이언트가 동일한 key로 락을 요청하면 동일한 락을 획득하려고 시도하게 된다
- tryLock을 통해 락을 획득하려고 시도한다. 해당 메서드는 락을 획득할때까지 최대 5초를 기다리고 1초마다 락을 획득하려 시도한다.
- 만약 락을 획득하는 것을 5초 동안 실패한다면 "Lock 획득 실패!" 라는 예외를 처리하게 된다
- 서비스 로직이 실행되면 finally에 있는 lock을 unlock시켜준다.
🎆 마치며
비관적 락은 각각의 락을 가져오고 blocking하기 때문에 요청이 많아지면 많아질수록 굉장히 안좋은 성능을 보인다. 동시성을 제어하는 이유가 뭘까? 물론 데이터의 일관성을 유지하고 무결성을 유지하는 것도 있지만 서버의 과부하를 막고 성능의 저하를 막고 서비스가 중단되는 것을 막기 위한 것도 있다. 그래서 Redis를 도입해보았다.
사실 Redis를 지금까지 사용하면서 엄청난 성능을 꾸준히 보여왔기에 기대가 엄청나게 컸다. 하지만 기대가 실망으로 변하는데는 그리 오래 걸리지 않았다.
Lettuce 방식에서 특히 심했는데 사실 Redis > DB 라는 생각이 많이 들어서 더 그랬다. 하지만 Lettuce의 단점이 얼마나 심각한 단점인지 확연히 알게 되었다
하지만 Pub/Sub 은 비관점 락보다 좋은성능! 이라고 하기보다는 Lettuce의 단점을 보완한 lock 방법에 초점을 두어야 한다는 것을 잘 알게 되었다 또한 비관적 락보다 성능이 월등히 좋아서 사용하는 것이 아니라는 것도 알게 되었다.
♪ 정리
Redis를 사용하여 동시성을 제어하는 것에 대한 이점
분산 시스템에서의 동시성 제어와 락 관리를 보다 효율적으로 처리하기 위해 사용된다. 또한 분산 환경에서 Lock 관리를 지원하기 때문에 여러 서버에서 락을 공유하고 동시성을 제어 할 수 있다. 마이크로 서비스 아키텍쳐와 같이 여러 인스턴스가 동작하는 환경이기 때문에 매우 중요하다
🎆 참고 블로그
'프로젝트 > 항해99 개인 프로젝트' 카테고리의 다른 글
🚢 Redis를 사용해서 선착순으로 쿠폰 발급하기 (0) | 2024.05.13 |
---|---|
🚢 중간테이블로 인한 JPA의 N+1문제 (0) | 2024.05.09 |
🚢 Feign Client 와 RestTemplate (0) | 2024.04.30 |
🚢 Open Feign을 사용하며 발생하는 서비스 간의 장애 처리 (0) | 2024.04.30 |
🚢 재고 관리를 위한 동시성 제어 (Monolithic Architecture) (2) | 2024.04.27 |