동시성 이슈 문제 - Redis 사용

yookyungmin·2023년 8월 13일
0

Lettuce

  • setnx 명령어를 활용하여 분산락 구현
    key 와 value를 set 할때 기존에 값이 없을 때만 set, spin lock 방식이므로 retry 로직을 개발자가 작성을 해줘야 함
  • spin lock 방식
    lock을 획득 하려는 스레드가 lock 을 사용할 수 있는 반복적으로 확인하면서 lock 획득을 시도하는 방식


  1. 스레드1이 key 1인 data를 redis에 set하려 할때, 처음에는 redis엔 key 1인 데이터가 없기 때문에 정상적으로 set.
  2. 그후에 스레드2가 key1인 data를 set 하려 할때 redis엔 이미 key가 1인 data가 있으므로 실패를 리턴, 실패를 하였기 때문에 일정 시간 이후에 lock 획득을 할때까지 재시도 하는 로직을 작성 해줘야 함.
  3. lettuce를 활용하는 방법은 mysql의 named Lock과 거의 비슷 다른점은 redis를 이용하는 것과 세션관리를 신경안써도 된다는 점이다

Repository

@Component
public class RedisLockRepository {
    private RedisTemplate<String, String> redisTemplate;
    public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    } //redis 명령어 실행 위한 템플릿 di
    public boolean lock(Long key){
        return redisTemplate
                .opsForValue()
                .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    } //key 에 사용할 변수를 받아야 하기 때문에 key 매개변수 추가
    public boolean unLock(Long key){
        return redisTemplate.delete(generateKey(key));
    }
    //로직 실행전에 key 와 setnx 명령어를 활용해서 lock을 하고 unlock 메시지를 통해 lock을 해제하는 방식
    private String generateKey(Long key){
        return key.toString();
    }
}
  1. redis 명령어를 이용할수 있어야 하기 때문에 레디스 레파지토리 생성
@Component
public class LettuceLockStockFacade {
    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;
    public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
        this.redisLockRepository = redisLockRepository;
        this.stockService = stockService;
    }
    public void decrease(Long id, Long quantity) throws InterruptedException {
        while(!redisLockRepository.lock(id)){
            Thread.sleep(100);
        }
        try{
            stockService.decrease(id, quantity);
        }finally {
            redisLockRepository.unLock(id);
        }
    }
}
  1. while문 활용해서 lock 획득 시도 lock 획득에 실패 하였다면 100밀리 세컨즈 텀을 두고 lock 재시도, redis 부하 줄이기
  2. lock 획득에 성공하였다면서 stockservice활용해서 재고 감소, 로직 종료시 unlock메서드로 lock 해제
    장점 : lettuce를 활용한 방법은 구현이 간단
    단점 : spin lock 방식으로는 redis 부하를 줄수 있어서 Thread.sleep()을 통해 재시도간에 텀을 둬야 한다.

Redisson

  • pub sub 기반으로 Lock 구현 제공
    채널을 하나를 만들고 lock을 점유중인 스레드가 lock 획득을 하려고 대기중인 스레드에게 해제를 알려주면 안내를 받은 스레드가 lock 획득을 시도하는 방식, lettuce랑 다르게 retry 로직을 작성하지 않아도됨.


redisson을 사용하기 위해 의존성 추가

pub/sub 실습을 위해 두개의 cli가 필요하다


1. 첫번째 터미널에서 subscribe ch1 명령어 통해 ch1 이라는 채널 구독
2. 두번째 터미널은 publish ch1 hello 명령어를 통해 hello 라는 메시지를 보내준다.
3. ch1 채널을 구독하는 터미널에서 hello 메시지를 받는다.
4. redisson 은 자신이 점유하고 있는 lock을 해제할때 채널에 메시지를 보내줌으로 lock 획득을 해야하는 스레드들에게 lock 획득을 하라고 전달. 메시지를 받은 스레드는 lock 획득을 시도.
5. lettuce는 계속 lock 획득을 시도하는 반면, redisson은 lock 해제가 되었을때 한번 혹은 몇번한 시도하고 pub/sub 형식이기에 redis의 부하를 줄여준다.

redisson 같은 경우엔 lock 관련 된 클래스를 라이브러리에서 제공해주므로 별도의 Repository는 작성할 필요 없지만 로직 실행 된 전 후로 lock 획득 해제는 해주어야 하므로 facade 클래스는 필요

@Component
public class RedissonLockStockFacade {
    private RedissonClient redissonClient;
    private StockService stockService;
    public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) {
        this.redissonClient = redissonClient;
        this.stockService = stockService;
    }
    public void decrease(Long id, Long quantity){
        RLock lock = redissonClient.getLock(id.toString()); //lock 객체 가져오기
        try{
            boolean available = lock.tryLock(15, 1, TimeUnit.SECONDS); //몇초동안 lock 획득 시도할것인지, 점유할것인지 설정
            if(!available){
                System.out.println("lock 획득 실패");
                return;
            }
            stockService.decrease(id, quantity);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally { //로직 정상 종료 시
            lock.unlock();
        }
    }

pub-sub 기반 구현이기에 redis의 부담을 줄여주지만, 구현이 조금 복잡하거나 별도의 라이브러리를 사용해야 하는 부담감이 있다.

정리

실무에서는 재시도가 필요하지 않은 lock 경우에는 lettuce를 활용
재시도가 필요한 경우에는 redisson 활용

재시도가 필요하지 않은 예시 :
유저 1명당 로지텍의 A 상품을 1개밖에 구매할 수 없다고 가정해보겠습니다.
이 상황에서 userid_로지텍A상품을 key 로 lock 을 잡은상태에서 동일한 요청이 들부어온다면 락을 기다릴 필요가 없을겁니다.
먼저 들어온 요청이 처리가 된다면 이후의 요청이 lock 을 잡더라도 할 일이 없기때문입니다.
이러한 상황이 재시도가 필요가 없는 상황입니다.

분산락이란?

서버가 2대가 있다고 가정해보겠습니다.
그렇다면 서버1 과 서버2에 각각 Redis 가 있는것이 아니라 서버 1과 서버 2가 Redis 가 있는 서버 3을 바라보는 것 입니다.

분산락이란 여러서버에서 공유된 데이터를 제어하기 위해 사용하는 기술이고 여러서버에서 레디스를 사용하여 공유된 데이터를 제어하고있기에 분산락이라고 표현하는 것입니다.

Mysql 과 비교해선 활용중인 Redis가 없다면 별도의 구축 비용과 인프라 관리비용이 필요
Mysql보다 성능이 좋다

참고
https://www.inflearn.com/course/lecture?courseSlug=%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C&unitId=114854&category=questionDetail

0개의 댓글