Redis

동시성문제 -> Redis Redisson

MIN우 2023. 7. 2. 12:53
728x90

재시도가 필요하지 않은 Lock은 Lettuce를 활용할 것이고 재시도가 필요한 경우에는 Redisson을 사용할 것 입니다. setnx 메소드는 만약 키가 존재하지 않는다면 설정하게 되는 것이므로 Redis 에 계속해서 LockKeyName 이 존재하는지 확인해야만 합니다. 따라서 순회하는 동안 계속해서 Redis 에 요청을 보내게 되는 것이므로 스레드 혹은 프로세스가 많다면 Redis 에 부하가 가게 될 것이기 때문에 재시도가 필요한 경우에는 Redisson 방식을 사용할 것입니다

 

Redisson

- Pub-Sub 기반으로 Lock 구현 제공

- Pub-Sub 방식이란, 채널을 하나 만들고, 락을 점유중인 스레드가, 락을 해제했음을, 대기중인 스레드

에게 알려주면 대기중인 스레드가 락 점유를 시도하는 방식

-이방식은, Lettuce 와 다르게 대부분 별도의 Retry하는 코드를 작성하지 않아도 된다! 

 

 

 

Pub-Sub 과정

 

Redisson의 Pub-Sub 실습해보자

 

의존성을 추가해주자

dependencies {
    implementation 'org.redisson:redisson-spring-boot-starter:3.17.6'  
}

 

RLock

redisson에는 RLock이라는 객체가 존재합니다. 이 객체를 통해 락을 컨트롤할 수 있습니다. RLock을 얻기 위해서는 RedissonClient.getLock() 메서드를 호출해주어야 합니다.

 

 

RedissonLockStockFacade

Redisson 같은 경우 Lock 과 관련된 클래스를제공해 줍니다 -> RedissonClient

 

@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {

    private final RedissonClient redissonClient;
    private final StockService stockService;

    public void decrease(final Long key, final Long quantity) {
        //key 로 Lock 객체 가져옴
        RLock lock = redissonClient.getLock(key.toString());

        try {
            //획득시도 시간, 락 점유 시간
            boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS);

            if (!available) {
                System.out.println("lock 획득 실패");
                return;
            }

            stockService.decrease(key, quantity);
            
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            lock.unlock();
        }
    }
}

 

기존의 서비스 레이어에서 분리하여 퍼사드(facade)를 사용한 이유?

서비스레이어에서 바로 Redisson
을 사용하게 되면 테스트 코드에 실패합니다. 그 이유는 분산락 해제 시점과 트랜잭션 커밋 시점의 불일치 때문입니다. 서비스 단의 decrese 메서드에는 @Transactional 어노테이션이 붙어 있습니다. 때문에 스프링 AOP를 통해 decrease 메서드 바깥으로 트랜잭션을 처리하는 프록시가 동작하게 됩니다. 반면 락 획득과 해제는 decrease 메서드 내부에서 일어납니다. 때문에 스레드 1과 스레드 2가 경합한다면 스레드 1의 락이 해제되고 트랜잭션 커밋이 되는 사이에 스레드 2가 락을 획득하게 되는 상황이 발생합니다. 데이터베이스 상으로 락이 존재하지 않기 때문에 스레드 2는 데이터를 읽어오게 되고, 스레드 1의 변경 내용은 유실됩니다. 때문에 📢락 범위가 트랜잭션 범위보다 크도록 Facade를 만들어주도록 하겠습니다.

 

 

Redisson 테스트

- 약 6초정도의 시간이 소요되었다

 

package com.example.stock.facade;

import com.example.stock.domain.Stock;
import com.example.stock.repository.StockRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.*;


@SpringBootTest
class RedissonLockStockFacadeTest {


    @Autowired
    private RedissonLockStockFacade redissonLockStockFacade;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before(){
        Stock stock = new Stock(1L, 1000L);

        stockRepository.save(stock);

    }
    @AfterEach
    public void after(){
        stockRepository.deleteAll();
    }



    @Test
    public void 동시에_1000개의_요청() throws InterruptedException {
        int threadCount=1000;
        ExecutorService executorService = Executors.newFixedThreadPool(32);

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for(int i=0;i<threadCount;i++){
            executorService.submit(()->{
                try {
                    redissonLockStockFacade.decrease(1L,1L);

                }finally {
                    countDownLatch.countDown();
                }

            });
        }
        countDownLatch.await();

        Stock stock = stockRepository.findById(1L).orElseThrow();

        assertEquals(0L,stock.getQuantity());



    }
}

 

 

 

 

Redisson의 특징

  • 락 획득 재시도를 기본으로 제공한다.
  • pub-sub 방식으로 구현이 되어있기 때문에 lettuce 와 비교했을 때 redis에 부하가 덜 간다.
  • 별도의 라이브러리를 사용해야한다.
  • lock을 라이브러리 차원에서 제공해주기 때문에 사용법을 공부해야 한다.
728x90