🔥 공대생은 성장 중/강의

[Spring & Java] 🚀 재고시스템으로 알아보는 동시성이슈 해결방법

민돌v 2022. 10. 6. 20:53
인프런 "재고시스템으로 알아보는 동시성 이슈 해결방법" 강의를 보고 정리한 글입니다.

📗 Spring & Java, Mysql, Redis 를 이용합니다

  • 재고시스템을 활용해서, 멀티스레드 혹은 분산환경에서 가변데이터에 접근하는 동시성 문제를 해결하는 내용이 강의에 담겼습니다.

 

동시성 문제란, 동일한 하나의 데이터에 2 이상의 스레드, 혹은 세션에서 가변 데이터를 동시에 제어할 때 나타는 문제로,

하나의 세션이 데이터를 수정 중일때, 다른 세션에서 수정 전의 데이터를 조회해 로직을 처리함으로써 데이터의 정합성이 깨지는 문제를 말합니다.


📌 강의를 듣고 느낀 점은, 동시성 문제의 근본적인 해결 방법은 가변데이터에 순차적으로 접근할 수 있는 방법을 구상하는 것 이라고 생각했습니다

  1. 데이터베이스를 이용한 락
  2. 프레임워크 혹은 언어 단에서의 Synchronized 사용
  3. 혹은 자료구조..? (큐가 생각남) 

 

[목차]

  1. 멀티스레드 환경에서 레이스 컨디션이 일어나는 이유
  2. 레이스 컨디션 해결방법
    1. Java synchronized - 동기화
    2. MySql Lock
      1. Pesimistic Lock
      2. Optimistic Lock
      3. Named Lock
    3. Redis Lock
      1. Lettuce Lock
      2. Redisson Lock

 


재고시스템 기본 로직

  • Stock : 재고를 가지고 있는 객체
  • StockRepoisoty : jpaRepository
  • StockService : 재고 감소 로직

 

강의에서는 간단한 재고감소 로직을 동시성문제를 다룹니다.

코드는 🙆🏼‍♂️깃허브에 있습니다!

 

🖥  Stock.class

재고를 가지고있는 Stock 객체

@Entity
@Getter
@NoArgsConstructor
public class Stock {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Long productId;
    private Long quantity;
    @Version
    private Long version;

    public Stock(final Long id, final Long quantity) {
        this.id = id;
        this.quantity = quantity;
    }

    public void decrease(final Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("재고 부족");
        }
        this.quantity = this.quantity - quantity;
    }
}

 

🖥  StockService.class

재고를 감소 비지니스로직을가지는 Servcie 레이어 

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    /**
     * 재고 감소
     */
    @Transactional
    public synchronized void decrease(final Long id, final Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
}

 


1.  동시성 문제

  • 🔝 위의 예제 코드는, 흔히 생각되는 단순 재고 감소 로직입니다. 실행 시 아무런 문제가 없지만
  • ⬇️ 아래와 같은 상황에서는 예상과는 다른 결과값을 도출합니다.

 

🖥 StockServiceTest.class

멀티 스레드 환경을 구현한 Service Test

 

📌 ExecutorService

  • ExecutorService란, 병렬 작업 시 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러입니다.
  • ExecutorService는 손쉽게 ThreadPool을 구성하고 Task를 실행하고 관리할 수 있는 역할을 합니다.
  • Executors 를 사용하여 ExecutorService 객체를 생성하며, 쓰레드 풀의 개수 및 종류를 지정할 수 있는 메소드를 제공합니다.

📌 CountDownLatch

  • CountDownLatch란, 어떤 스레드가 다른 쓰레드에서 작업이 완료될 때 가지 기다릴 수 있도록 해주는 클래스입니다.
  • CountDownLatch 를 이용하여, 멀티스레드가 100번 작업이 모두 완료한 후, 테스트를 하도록 기다리게 합니다.
  • CountDownLatch 작동원리
    1.  new CountDownLatch(5); 를 이용해 Latch할 갯수를 지정합니다.
    2. countDown()을 호출하면 Latch의 숫자가 1개씩 감소합니다.
    3. await() 은 Latch의 숫자가 0이 될 때 까지 기다리는 코드입니다.
@Test
public void 동시에_100개의_요청() throws InterruptedException {
    int threadCount = 100;
    //멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있또록 해주는 java api
    ExecutorService executorService = Executors.newFixedThreadPool(32);

    //다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록 도와주는 API - 요청이 끝날때 까지 기다림
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.submit(() -> {
                try {
                    stockService.decrease(1L, 1L);
                }
                finally {
                    latch.countDown();
                }
            }
        );
    }

    latch.await();

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

    //100 - (1*100) = 0
    assertThat(stock.getQuantity()).isEqualTo(0L);

}

테스트 실패

 

👏🏻 테스트 실패

  • CountDownLatch를 이용하여, 멀티스레드 작업이 100번의 재고감소 로직을 호출한 뒤에 재고가 0이 되는지 확인했지만, 실제로는 의외의 값이 나옵니다.
  • 그 이유는 레이스 컨디션(Race Condition) 이 일어나기 때문입니다.
  • 🔥 레이스 컨디션이란, 2 이상의 스레드가 공유 데이터에 액세스 할 수 있고, 동시에 변경하려할 떄 발생할 수 있는 문제

 

 


 

2. 멀티스레드로 처리할 때 레이스 컨디션이 일어나는 이유

📗 예상 작업 순서

  • 우리가 멀티스레드로 작업을 할 때, 예상한건 아래의 그림처럼 순차적으로 데이터에 접근해 처리하고,
  • 처리된 데이터를 다음 스레드가 접근하여 처리하는 그림입니다.

예상했던 멀티스레드 작업 과정

📕 실제 작업 순서

  • 하지만, 실제로는 다릅니다.
  • 아래의 그림처런, 같은 데이터를 동시에 변경 (공유된 가변 데이터) 하려 하기 때문에 작업 중 하나가 누락되게 될 수 있습니다.


3. 레이스 컨디션 해결 방법

  • Race Condition 을 해결하기 위해서는,
  • 👏🏻 하나의 스레드가 작업을 완료한 후에, 다른 스레드가 공유된 자원에 접근 가능하도록 해야 합니다.

 

Race Condition을 해결하는 여러 방법들


1) Synchronized 이용

  • Synchronized를 메소드에 명시해주면 하나의 스레드만 접근이 가능합니다.
  • 멀티스레드 환경에서 스레드간 데이터 동기화를 시켜주기 위해서 자바에서 제공하는 키워드 입니다.
  • 공유되는 데이터의 Thread-safe를 하기 위해, synchronized 로 스레드간 동기화를 시켜 thread-safe 하게 만들어줍니다.
  • 📌 자바에서 지원하는 synchronized는,  현제 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터 접근을 막아 순차적으로 데이터에 접근할 수 있도록 해줍니다.

(근데 강의에서는 @Transaction이 붙지 않아야만 성공함?!?!)

/**
 * 재고 감소
 */
@Transactional
public synchronized void decrease(final Long id, final Long quantity) {
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);
    stockRepository.saveAndFlush(stock);
}

테스트코드를 수정하지 않고, 로직도 변경하지 않은상태임에도 테스트가 통과되는 모습을 볼 수 있었습니다.

➡️ saveAndFlush 사용 이유

 


 

JAVA Sychronized 의 문제점

  • 자바의 Sychronized는 하나의 프로세스 안에서만 보장이 됩니다.
  • 📌 즉, 서버가 1대일때는 문제가 없지만 서버가 2대 이상일 경우 데이터에 대한 접근을 막을 수가 없습니다.

 

서버가 여러대 일 경우, Synchronized 는 각 프로세스의 동시접근 제어만을 보장해주기 때문에

다른 서버에서 가변 공유데이터에 접근하는 것을 막을 수 가 없어, 업데이트 도중 값이 변경될 수 있는 문제점이 여전히 남아 있습니다.

 


2) DataBase 이용하기

Mysql 을 활용하는 방법 (Lock)

  • 2번째 방법은 DataBase Lock 을 이용해 순차적인 접근으로 제어하는 방법입니다.

 

1. Pessimistic Lock

  • 실제로 데이터에 Lock을 걸어서 정합성을 맞추는 방법입니다.
  • exclusive lock(베타적 잠금) 을 걸게되면 다른 트랜잭션에서는 lock 이 해제되기전에 데이터를 가져갈 수 없게됩니다.
  • ✨ 자원 요청에 따른 동시성문제가 발생할 것이라고 예상하고 락을 걸어버리는 비관적 락 방식입니다.
  • 하지만, 데드락이 걸릴 수 있기 때문에 주의하여 사용해야합니다.

Pessimistic Lock

예를들어 Server 1 DB 데이터를 가져올 떄, Lock 을 걸어버리면, 다른 서버에서는 Server1의 작업이 끝나 락이 풀릴 때 까지, 데이터에 접근하지 못하게 됩니다.

📌 결국 Pesimistic Lock이란, 데이터에는 락을 가진 스레드만 접근이 가능하도록 제어하는 방법입니다.


2. Optimisitc Lock

  • 실제로 Lock 을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법입니다.
  • 먼저 데이터를 읽은 후에 update 를 수행할 떄 현재 내가 읽은 버전이 맞는지 확인하며 업데이트 합니다.
  • ✨ 자원에 락을 걸어서 선점하지 않고, 동시성 문제가 발생하면 그때가서 처리하는 낙관적 락 방식입니다.
  • 내가 읽은 버전에서 수정사항이 생겼을 경우에는 application에서 다시 읽은 후에 작업을 수행하는 롤백 작업을 수행해야 합니다.

 

[과정]

1) 서버 1이 version1 임을 조건절에 명시하면서 업데이트 쿼리를 날립니다.

2) version1 쿼리가 업데이트 되어서, 디비는 version 2가 됩니다.

Optimisitc Lock 점유 과정

3) server2 가 version1 로 업데이트 쿼리를 날리면 버전이 맞지않아 실패합니다.

4) 쿼리가 실패하면 server2 에서 다시 조회하여 버전을 맞춘 후 업데이트 쿼리를 날리는 과정을 거칩니다.

Optimisitc Lock 점유 과정

 

3. Named Lock

  • 이름을 가진 metadata locking 입니다. 이름을 가진 lock 을 획득한 후 해제할 때 까지 다른 세션은 이 lock 을 획득할 수 없도록 합니다.
  • 주의할 점으로는 transaction 이 종료될 떄 lock 이 자동으로 해제되지 않습니다.
  • 별도의 명령어로 해제를 수행해주거나 선점시간이 끝나야 해제됩니다.

📌 Named Lock은 Passimistic Lock 과 유사하지만, Passimistic Lock 은 row 나 table 단위로 락을 걸지만, Named Lock 은 metadata 단위로 락을 건다는 차이점이 존재합니다.

 

 


1. Pessimistic Lock 사용 하기

  • 실제 데이터베이스에 락을 걸어 정합성을 맞추는 방법
  • [exclusive lock 시 데이터 점유 과정]

Pessimistic Lock

 

🖥 StockRepository interface

  • Repository 에 데이터 접근시 락을 거는 쿼리를 작성합니다.
public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByWithPessimisticLock(final Long id);
}

📙 Pessimistic Lock 의 장점

  1. 충돌이 빈번하게 일어난다면 롤백의 횟수를 줄일 수 있기 때문에, Optimistic Lock 보다는 성능이 좋을 수 있습니다.
  2. 비관적 락을 통해 데이터를 제어하기 때문에 데이터 정합성을 어느정도 보장할 수 있습니다.

 

📘 Pessimistic Lock 의 단점

  1. 데이터 자체에 별도의 락을 잡기때문에 동시성이 떨어져 성능저하가 발생할 수 있습니다.
  2. 특히 읽기가 많이 이루어지는 데이터베이스의 경우에는 손해가 더 크다고 합니다.
  3. 서로 자원이 필요한 경우, 락이 걸려있으므로 데드락이 일어날 가능성이 있습니다.

 

 

2. Optimistic Lock 사용 하기

  • Optimistic Lock 은 실제 락을 사용하지 않고, 버전을 이용해서 락과 유사한 과정을 가지는 논리적인 락이라고 생각이 듭니다.

 

[Optimistic lock 시 데이터 점유 과정]

Optimistic lock

 

🖥Stock.class

  • 버전 컬럼을 추가해야 합니다.
Entity
@Getter
@NoArgsConstructor
public class Stock {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private Long productId;

    private Long quantity;

	//버전 칼럼 추가
    @Version
    private Long version;

    //로직 생략

}

 

🖥 StockRepository

  • 버전을 확인하며 데이터를 처리합니다.
public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByWithPessimisticLock(final Long id);

	//Optimistic Lock
    @Lock(value = LockModeType.OPTIMISTIC)
    @Query("select s from Stock s where s.id = :id")
    Stock findByWithOptimisticLock(final Long id);

}

📙 Optimistic Lock 의 장점

 

  • 충돌이 안난다는 가정하에, 별도의 락을 잡지 않으므로 Pessimistic Lock 보다는 성능적 이점을 가집니다.

 

📘Optimistic Lock 의 단점

  • 업데이트가 실패했을 떄, 재시도 로직을 개발자가 직접 작성해 주어야 합니다.
  • 충돌이 빈번하게 일어나거나 예상이되면, 롤백처리를 해주어야하기 때문에 Pessimistic Lock 이 더 성능이 좋을 수도 있습니다. 

 

 

3. Named Lock 사용 하기

  • Named Lock은 이름을 가진 metadata Lock 입니다.
  • 이름을 가진 락을 획득한 후, 해지될때 까지 다른 세션은 이 락을 획득할 수 없게 됩니다.
  • 주의할 점은, 트랜잭션이 종료될 떄 락이 자동으로 해지되지 않기 떄문에, 별도로 해지해주거나 선점시간이 끝나야 해지됩니다.
  • Mysql 에서는 getLock( ) 을 통해 획들 / releaseLock() 으로 해지 할 수 있습니다.

 

[Named lock 시 Lock 점유 과정]

  1. Named Lock은 Stock에 락을 걸지 않고, 별도의 공간에 락을 겁니다.
  2. session-1 이 1이라는 이름으로 락을 건다면, session 1 이 1을 해지한 후에 락을 얻을 수 있습니다.

Named Lock 점유 과정

 

⚡️ Named Lock 사용시 주의사항

  • 예제에서는 동일한 DataSource 를 사용하지만, 실제 서비스에서는 커넥션풀이 부족해질 수 있기에 DataSoruce 를 분리하는 걸 추천한다고 합니다.

 

🖥LockRepository 

  • 예제에서는 편의성을 위해서 Stock 엔티티를 사용하지만, 실무에서는 별도의 JDBC 를 사용해야 한다고 합니다.
public interface LockRepository extends JpaRepository<Stock, Long> {

    @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
    void getLock(String key);

    @Query(value = "select release_lock(:key, key)", nativeQuery = true)
    void releaseLock(String key);
}

 

🖥 NamedLockFacde

  • StockService 는 부모의 트랜잭션과 별도로 실행되어야하기 때문에 propergation을 별도로 생성해줍니다
  • 부모의 트랜잭션과 동일한 범위로 묶인다면 Synchronized 와 같은 문제인 DataBase에 commit 되기전에 락이 풀리는 현상이 발생합니다.
  • 그렇기 때문에 별도의 트랜잭션으로 분리해서 DataBase에 정상적으로 Commit이 된 후에 락을 해제해 주려는 의도르 품고있다고합니다.
  • 핵심은 Lock을 해제하기전에 DataBase에 Commit이 되도록 하는것..!!
@Component
@RequiredArgsConstructor
public class NamedLockFacade {

    private final LockRepository lockRepository;
    private final StockService stockService;

    //부모의 트랜잭션과 별도로 실행되어야 함
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrease(final Long id, final Long quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrease(id, quantity);
        }finally {
            //락의 해제
            lockRepository.releaseLock(id.toString());
        }
    }
}

락을 획득한 후, 비지니스 로직을 처리합니다.

그 후 finally 에서 락을 해지해줍니다.

 

그리구 예제에서는 같은 DataSource 를 사용해주어야하기 때문에 커넥션 풀 수를 늘려주어야 합니다.

spring:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
  datasource:
    driver-class-name: org.h2.Driver
    url:  jdbc:h2:tcp://localhost/~/test
    username: sa
    password:
    hikari:
      maximum-pool-size: 40

 

🖥 NamedFacadeTest

@SpringBootTest
class NamedLockFacadeTest {

    @Autowired
    private NamedLockFacade stockFacade;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before() {
        Stock stock = new Stock(1L, 100L);
        stockRepository.saveAndFlush(stock);
    }

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


    @Test
    @DisplayName("Pessimistic LOCK 동시에_100개의_요청")
    public void Pessimistic_requests_100_AtTheSameTime() throws InterruptedException {
        int threadCount = 100;
        //멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있또록 해주는 java api
        ExecutorService executorService = Executors.newFixedThreadPool(32);

        //다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록 도와주는 API - 요청이 끝날때 까지 기다림
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                    try {
                        stockFacade.decrease(1L, 1L);
                    } finally {
                        latch.countDown();
                    }
                }
            );
        }

        latch.await();

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

        //100 - (1*100) = 0
        assertThat(stock.getQuantity()).isEqualTo(0L);
    }
}

 

📙 Named Lock 이 장점

  • 📌 NamedLock 은 주로 분산락을 구현할 때 사용합니다.
  • Pessimistic 락은 time out을 구현하기 굉장히 힘들지만, Named Lock은 비교적 손쉽게 구현할 수 있다고 합니다.
  • 그 외에, 데이터 정합성을 맞춰야하는 경우에도 사용할 수 있다고 합니다.

 

📘 Named Lock 이 단점

  • 하지만., Naemd Lock 은 트랜잭션 종료 시에, 락 해제와 세션관리를 잘 해주어야하므로 주의해서 사용주어야 합니다.
  • 또 실제 사용할 때는 구현방법이, 복잡할 수 있습니다.

 


4. Redis 이용해보기

  • Redis 를 사용하여 동시성 문제를 해결하는 대표적인 라이브러리 2가지가 존재합니다.
    1. Lettuce
    2. Redisson

 

1. Lettuce

  • Setnx 명령어를 활용하여 분산락을 구현 (Set if not Exist - key:value를 Set 할 떄. 기존의 값이 없을 때만 Set 하는 명령어)
  • Setnx 는 Spin Lock방식이므로 retry 로직을 개발자가 작성해 주어야합니다.
  • Spin Lock 이란, Lock 을 획득하려는 스레드가 Lock을 획득할 수 있는지 확인하면서 반복적으로 시도하는 방법입니다.

 

📌 Spin Lock 과정

📌 Spin Lock 과정

2. Redisson

  • Pub-sub 기반으로 Lock 구현 제공
  • Pub-Sub 방식이란, 채널을 하나 만들고, 락을 점유중인 스레드가, 락을 해제했음을, 대기중인 스레드에게 알려주면 대기중인 스레드가 락 점유를 시도하는 방식입니다.
  • 이 방식은, Lettuce와 다르게 대부분 별도의 Retry 방식을 작성하지 않아도 됩니다.

 

📌 Pub-Sub 과정

Redisson 점유 과정

 


Redis 환경 설정

1) 먼저 도커로 레디스를 다운받아 실행해줍니다.

  1. redis 이미지 다운로드 :   docker pull redis
  2. redis 실행 :  docker run --name myredis -d -p 6379:6379 redis
  3. 실행 확인 명령어 :  docker ps

 

2) Spring Gradle Redis 의존성을 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //redis 의존성 추가
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

 

 

✅ Lettuce 사용하기

  • Lettuce를 작성하여 재고감소 로직 작성하기

 

1) setnx 명령어 사용해보기

먼저, redis cli를 이용하여 setnx 명령어를 사용해보겠습니다.

  1. 먼저 컨테이너 id 를 사용하여 cli 로 접속합니다.

2. setnx 명령어는 key 와 value 로 삽입합니다.

  • setnx (key) (value)
  • 1 이라는 key 로 맨처음 삽입할때는 성공하지만, 그 이후로는 실패합니다.
  • del (key) 명렁어로 해당 key의 데이터를 삭제하고 다시 삽입하면 성공합니다.

 

📌 Lettuce를 활용하는 방법은 Mysql 의 NamedLock 과 유사하지만, Redis 를 활용한다는 점, 세션관리에 신경쓰지 않아도 된다는 차이점이 있습니다. (왜 세션관리 안해두됨????_??? )

 

2) Spring 에서 Redis Lettuce 방식 사용

🖥 RedisLockRepository

  • key 를 이용한 Lock 과 unLock 메소드 정의
@Component
@RequiredArgsConstructor
public class RedisLockRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public Boolean lock(final Long key) {
        return redisTemplate
            .opsForValue()
            //setnx 명령어 사용 - key(key) value("lock")
            .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    }

    public Boolean unlock(final Long key) {
        return redisTemplate.delete(generateKey(key));
    }

    private String generateKey(final Long key) {
        return key.toString();
    }
}
  1. SpinLock 방식으로 락을 얻기를 시도하고,
  2. 락을 얻은 후, 재고 감소 비지니스 로직을 처리합니다.
  3. 그 후, 락을 해제해주는 방식이 Lettuce 방식입니다.
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade  {

    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;

    public void decrease(final Long key, final Long quantity) throws InterruptedException {
        // Lock 획득 시도
        while (!redisLockRepository.lock(key)) {
            //SpinLock 방식이 redis 에게 주는 부하를 줄여주기위한 sleep
            Thread.sleep(100);
        }

        //lock 획득 성공시
        try{
            stockService.decrease(key,quantity);
        }finally {
            //락 해제
            redisLockRepository.unlock(key);
        }
    }
}

 

📌Sprin Lock 방식이, Lock 을 얻을 떄까지 Lock 얻기를 시도하기 떄문에, 계속해서 Redis 에 접근해서 Redis에 부하를 줄 수 있다는 단점이 존재합니다.

 


✅ Redisson 사용하기

  • Redisson 를 작성하여 재고감소 로직 작성하기

 

redisson 의존성 추가

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

 

1) Pub-sub 사용해보기

  • pub-sub 방식을 사용해보기 위해서는 2개의 redis cli가 필요합니다.

 

1. (1번 cli) subscribe 명령어를 사용하여 ch1 을 구독합니다.

subscribe ch1

 

2. (2번 Cli) publish 명령어를 사용하여 메세지를 전달합니다.

publish ch1 hello

3. (1번 cli) 1번 은 ch1 을 구독하고 있기 때문에, 메세지가 옴을 확인할 수 있습니다.

 

✨ Lettuce 와 다르게 Redisson 은 계속 락 획득을 계속 시도하는게 아니기 때문에 Redis의 부하를 줄일 수 있습니다.

 

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

🖥 RedissonLockStockFacade

@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();
        }
    }
}

 

 


👏🏻 Lettuce vs Redisson

Lettuce

  • 구현이 간단하다
  • Spring data redis를 이용하면 lettuce가 기본이기 떄문에 별도의 라이브러리를 사용하지 않아도 된다.
  • Spin Lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있다.

 

Redisson

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

 

 

 

 

 

 

강의 정리 끝!!

  • 📌 Lettuce를 활용한다면 세션관리에 신경쓰지 않아도 되는 이유를 잘 모르겠습니다 ㅠ
  • 📌 그 외에, 방법에서는 결국 동시성 문제의 근본적인 해결방안은 데이터에 순차적인 접근으로 통제함으로써 동기화를 시켜주는게 초점이다라는 것을 느낄 수 있었던 강의였습니다 😁

참고