BackEnd🌱/Java

μžλ°”μ—μ„œ λ™μ‹œμ„±μ„ ν•΄κ²°ν•˜λŠ” λ‹€μ–‘ν•œ 방법과 Redis의 뢄산락

dkswnkk 2023. 1. 8. 17:30

이번 ν¬μŠ€νŒ…μ€ μ‚¬μ „μ§€μ‹μœΌλ‘œ 운영체제의 동기화 이둠에 λŒ€ν•΄ μ•Œκ³  μžˆμ–΄μ•Ό μ†μ‰½κ²Œ 이해할 수 μžˆμœΌλ―€λ‘œ, ν—·κ°ˆλ¦¬μ‹œλŠ” 뢄듀은 μ•„λž˜ ν¬μŠ€νŒ…μ„ λ¨Όμ € 읽고 이번 ν¬μŠ€νŒ…μ„ μ½μ–΄μ£Όμ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€.

 

[OS] ν”„λ‘œμ„ΈμŠ€ 동기화(Process Synchronization)

μ„œλ‘  ν˜‘λ ₯적 ν”„λ‘œμ„ΈμŠ€λŠ” μ‹œμŠ€ν…œ λ‚΄μ—μ„œ μ‹€ν–‰ 쀑인 λ‹€λ₯Έ ν”„λ‘œμ„ΈμŠ€μ˜ 싀행에 영ν–₯을 μ£Όκ±°λ‚˜ 영ν–₯을 λ°›λŠ” ν”„λ‘œμ„ΈμŠ€μž…λ‹ˆλ‹€. ν˜‘λ ₯적 ν”„λ‘œμ„ΈμŠ€λŠ” 논리 μ£Όμ†Œ 곡간(즉, μ½”λ“œ 및 데이터)을 직접 κ³΅μœ ν•˜κ±°

dkswnkk.tistory.com

κ³΅μœ μžμ›μ— λŒ€ν•΄ λ™μ‹œμ— μ—¬λŸ¬ 개의 ν”„λ‘œμ„ΈμŠ€κ°€ μ ‘κ·Όν•˜μ—¬ μƒκΈ°λŠ” 경쟁 상황(race condition)을 μš°λ¦¬λŠ” λ™μ‹œμ„± λ¬Έμ œλΌκ³ λ„ ν•˜λ©°, 더 μžμ„ΈνžˆλŠ” λ™μΌν•œ ν•˜λ‚˜μ˜ 데이터에 두 개 μ΄μƒμ˜ μŠ€λ ˆλ“œ, ν˜Ήμ€ μ„Έμ…˜μ—μ„œ κ°€λ³€ 데이터λ₯Ό λ™μ‹œμ— μ œμ–΄ν•  λ•Œ λ‚˜νƒ€λŠ” 문제둜, ν•˜λ‚˜μ˜ μ„Έμ…˜μ΄ 데이터λ₯Ό μˆ˜μ • 쀑일 λ•Œ, λ‹€λ₯Έ μ„Έμ…˜μ—μ„œ μˆ˜μ • μ „μ˜ 데이터λ₯Ό μ‘°νšŒν•΄ λ‘œμ§μ„ μ²˜λ¦¬ν•¨μœΌλ‘œμ¨ λ°μ΄ν„°μ˜ 정합성이 κΉ¨μ§€λŠ” 문제λ₯Ό λ§ν•©λ‹ˆλ‹€

λ°±μ—”λ“œ 즉 μ„œλ²„κ°œλ°œμžμ—κ²Œ μ΄λŸ¬ν•œ λ™μ‹œμ„± λ¬Έμ œλŠ” 맀우 μ€‘μš”ν•œλ°, μœ„μ˜ 포슀트 μ„œλ¬Έμ• λ„ λ‚˜μ™€μžˆλ“―μ΄ λ™μ‹œμ„± 문제λ₯Ό μ²˜λ¦¬ν•˜μ§€ μ•ŠμœΌλ©΄ μ•„λž˜μ™€ 같은 상황이 λ°œμƒν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

λ™μ‹œμ„± 문제의 λŒ€ν‘œμ μΈ 예

  1. 곡유 μžμ›μΈ μ „μ—­ λ³€μˆ˜ 예금 10만 원이 μžˆλ‹€κ³  κ°€μ •ν•œλ‹€.
  2. ν”„λ‘œμ„ΈμŠ€ P1은 μ˜ˆκΈˆ 10만 원을 ν™•μΈν•œ μƒν™©μ—μ„œ ν”„λ‘œμ„ΈμŠ€ P2κ°€ μ˜ˆκΈˆ 5만 원을 μž…κΈˆν•˜μ—¬ μ΄ 15만 μ›μ˜ μ˜ˆκΈˆμ„ μ €μž₯ν•œλ‹€.
  3. ν•˜μ§€λ§Œ ν”„λ‘œμ„ΈμŠ€ P1μž…μž₯μ—μ„œλŠ” 아직 예금이 10만 원 μ΄κΈ°μ— 10만 원을 μΆ”κ°€ν•˜λ”λΌλ„ 15 + 10 = 25λΌλŠ” κ²°κ³Όκ°€ μ•„λ‹Œ 10 + 10 = 20μ΄λΌλŠ” 총예금이 μ €μž₯되게 λœλ‹€.

κ·Έλ ‡λ‹€λ©΄ 이제 μ–΄λ–»κ²Œ μ΄λŸ¬ν•œ λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμ„μ§€ ν•œλ²ˆ μž¬κ³ μ‹œμŠ€ν…œμ΄λΌλŠ” κ°„λ‹¨ν•œ λ‘œμ§μ„ μž‘μ„±ν•˜μ—¬ 정리해 λ³΄κ² μŠ΅λ‹ˆλ‹€. 

ν…ŒμŠ€νŠΈ ν™˜κ²½μ€ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • Apple Silicon (M1)
  • Java 11, Spring, JPA, Lombok
  • JUnit5
  • MySQL, Redis

μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 κ°€λŠ₯ν•©λ‹ˆλ‹€.

λͺ©μ°¨λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. μž¬κ³ μ‹œμŠ€ν…œ 기초 둜직
  2. λ™μ‹œμ„±μ„ κ³ λ €ν•˜μ§€ μ•Šμ€ 둜직
  3. Java Synchronized ν‚€μ›Œλ“œλ₯Ό ν™œμš©ν•œ 동기화 둜직
  4. DBμ—μ„œ μ œμ–΄ν•˜λŠ” 방법
    1. Pessimistic Lock을 ν™œμš©ν•œ 동기화 둜직
    2. Optimistic Lock을 ν™œμš©ν•œ 동기화 둜직
    3. Named Lock을 ν™œμš©ν•œ 동기화 둜직
  5. Redisλ₯Ό ν™œμš©ν•˜λŠ” 방법
    1. Redis Lettuce을 ν™œμš©ν•œ λΆ„μ‚° 락 κ΅¬ν˜„
    2. Redis Redission을 ν™œμš©ν•œ λΆ„μ‚° λ½ κ΅¬ν˜„

 

1. μž¬κ³ μ‹œμŠ€ν…œ 기초 둜직

λ¨Όμ € Entity, Service, RepositoryλŠ” λ‹€μŒκ³Ό 같은 μˆœμ„œλ‘œ κ°„λ‹¨ν•˜κ²Œ κ΅¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {

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

    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 -= quantity;
    }
    
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
public interface StockRepository extends JpaRepository<Stock, Long> {

}

 

2. λ™μ‹œμ„±μ„ κ³ λ €ν•˜μ§€ μ•Šμ€ 둜직

λ¨Όμ € λ™μ‹œμ„±μ„ κ³ λ €ν•˜μ§€ μ•Šμ€ 둜직 λ¨Όμ € μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€. 

@SpringBootTest
class StockServiceTest {

    @Autowired
    private StockService stockService;

    @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("race condition μΌμ–΄λ‚˜λŠ” ν…ŒμŠ€νŠΈ")
    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();
        assertEquals(0L, stock.getQuantity());

    }
}

μ΄ˆκΈ°μ— ProductIdκ°€ 1인 μž¬ν’ˆμ˜ μˆ˜λŸ‰μ„ 100개둜 μ„ΈνŒ…ν•˜μ—¬ DB에 μ„ΈνŒ…μ‹œμΌ°κ³ , ExecutorServiceλ₯Ό 톡해 32개의 κ³ μ •λœ μŠ€λ ˆλ“œν’€μ„ μƒμ„±ν•˜μ—¬ submitλ©”μ„œλ“œλ₯Ό 톡해 λ©€ν‹°μŠ€λ ˆλ“œλ‘œ λ™μž‘ν•˜μ—¬ 재고λ₯Ό κ°μ†Œμ‹œν‚€λŠ” λ‘œμ§μ„ μˆ˜ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

ExecutorService와 CountDownLatch에 λŒ€ν•΄ μƒμ†Œν•˜μ‹  뢄듀을 μœ„ν•΄ κ°„λž΅ν•˜κ²Œ μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

ExecutorService

  • ExecutorServiceλž€, 병렬 μž‘μ—… μ‹œ μ—¬λŸ¬ 개의 μž‘μ—…μ„ 효율적으둜 μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ μ œκ³΅λ˜λŠ” JAVA API이닀.
  • ExecutorServiceλŠ” μ†μ‰½κ²Œ ThreadPool을 κ΅¬μ„±ν•˜κ³  Taskλ₯Ό μ‹€ν–‰ν•˜κ³  관리할 수 μžˆλŠ” 역할을 ν•œλ‹€.
  • Executorsλ₯Ό μ‚¬μš©ν•˜μ—¬ ExecutorService 객체λ₯Ό μƒμ„±ν•˜λ©°, μŠ€λ ˆλ“œ ν’€μ˜ 개수 및 μ’…λ₯˜λ₯Ό 지정할 수 μžˆλŠ” λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•œλ‹€.

CountDownLatch

  • CountDownLatchλž€, μ–΄λ–€ μŠ€λ ˆλ“œκ°€ λ‹€λ₯Έ μŠ€λ ˆλ“œμ—μ„œ μž‘μ—…μ΄ μ™„λ£Œλ  λ•Œ 가지 기닀릴 수 μžˆλ„λ‘ ν•΄μ£ΌλŠ” ν΄λž˜μŠ€μ΄λ‹€.
  • CountDownLatchλ₯Ό μ΄μš©ν•˜μ—¬, λ©€ν‹°μŠ€λ ˆλ“œκ°€ 남은 횟수의 μž‘μ—…μ„ λͺ¨λ‘ μ™„λ£Œν•œ ν›„, ν…ŒμŠ€νŠΈλ₯Ό ν•˜λ„λ‘ κΈ°λ‹€λ¦¬κ²Œ ν•©λ‹ˆλ‹€.
  • CountDownLatch μž‘λ™μ›λ¦¬
    1. new CountDownLatch(10); 의 ν˜•μ‹μœΌλ‘œ Latch ν•  개수λ₯Ό μ§€μ •ν•œλ‹€.
    2. countDown()을 ν˜ΈμΆœν•˜λ©΄ Latch의 μΉ΄μš΄ν„°κ°€ 1κ°œμ”© κ°μ†Œν•œλ‹€.
    3. await()은 Latch의 μΉ΄μš΄ν„°κ°€ 0이 될 λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦°λ‹€.

μœ„ 둜직의 μˆ˜ν–‰ κ²°κ³ΌλŠ” λ‹€μŒκ³Ό 같이 μ‹€νŒ¨λ‘œ λ‚˜μ˜΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

100개의 재고λ₯Ό 1κ°œμ”© 100번 κ°μ†Œμ‹œμΌ°λ˜ 만큼 μˆ˜ν–‰κ²°κ³Ό 0μ΄λΌλŠ” 재고의 μˆ˜λŸ‰μ„ κΈ°λŒ€ν–ˆμœΌλ‚˜ 96μ΄λΌλŠ” ν„°λ¬΄λ‹ˆμ—†λŠ” μˆ˜λŸ‰μ΄ λ‚¨μ•„μžˆμŠ΅λ‹ˆλ‹€. μ΄μœ λŠ” μ„œλ‘μ— λͺ…μ‹œν–ˆλ˜ 은행 μž…μΆœκΈˆ μ˜ˆμ‹œμΈ race condition(κ²½μŸμƒν™©)이 일어났기 λ•Œλ¬ΈμΈλ°, 두 개 μ΄μƒμ˜ μŠ€λ ˆλ“œκ°€ 곡유 μžμ›μ— λŒ€ν•΄μ„œ λ™μ‹œμ— λ³€κ²½ν•˜λ €κ³  ν–ˆκΈ° λ•Œλ¬Έμ— μž…λ‹ˆλ‹€.

race condition을 ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” 근본적으둜 κ³΅μœ μžμ›μ— λŒ€ν•΄ ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œκ°€ μž‘μ—…μ„ μ™„λ£Œν•œ 후에 λ‹€λ₯Έ μŠ€λ ˆλ“œκ°€ μ ‘κ·Όν•  수 μžˆλ„λ‘ μ„€μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.

 

3. Synchronized ν‚€μ›Œλ“œλ₯Ό ν™œμš©ν•œ 동기화 둜직

λ¨Όμ € Synchronizedν‚€μ›Œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    /**
     * synchronized μ‚¬μš©μ‹œ @Transactionalκ³Ό λ™μ‹œμ— μ‚¬μš©ν•˜λ©΄ μ•ˆλœλ‹€.
     * Synchronizedλ₯Ό μ‚¬μš©ν•˜λŠ” μ΄μœ λŠ” ν•΄λ‹Ή λ©”μ†Œλ“œλ₯Ό ν•œ μ“°λ ˆλ“œμ—μ„œλ§Œ 돌리기 μœ„ν•΄μ„œλ‹€.
     * ν•˜μ§€λ§Œ, νŠΈλžœμž­μ…˜μ΄ 같이 μ •μ˜κ°€ λ˜μ–΄μžˆλ‹€λ©΄ 첫 번째 μ“°λ ˆλ“œκ°€ λλ‚˜κΈ° μ „ 두 번째 μ“°λ ˆλ“œκ°€ λ°œλ™ν•  μˆ˜λ„ μžˆλ‹€.
     */
    public synchronized void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
    @Test
    @DisplayName("synchronized μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        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();
        assertEquals(0L, stock.getQuantity());
    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

μœ„μ™€ 같이 κ°„λ‹¨ν•˜κ²Œ Serviceλ‘œμ§μ—μ„œ λ©”μ„œλ“œμ— synchronized ν‚€μ›Œλ“œλ₯Ό μΆ”κ°€ν•˜κ³ , @Transactional을 μ œκ±°ν•¨μœΌλ‘œμ¨ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • synchronizedλ₯Ό μ΄μš©ν•˜λ©΄ ν˜„μž¬ μ ‘κ·Όν•˜κ³  μžˆλŠ” λ©”μ„œλ“œμ— ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œλ§Œ μ ‘κ·Όν•  수 μžˆλ„λ‘ μž‘λ™ν•©λ‹ˆλ‹€.
  • μžλ°”μ—μ„œ μ§€μ›ν•˜λŠ” synchronizedλŠ”,  ν˜„μž¬ 데이터λ₯Ό μ‚¬μš©ν•˜κ³  μžˆλŠ” ν•΄λ‹Ή μŠ€λ ˆλ“œλ₯Ό μ œμ™Έν•˜κ³  λ‚˜λ¨Έμ§€ μŠ€λ ˆλ“œλ“€μ€ 데이터 접근을 막아 순차적으둜 데이터에 μ ‘κ·Όν•  수 μžˆλ„λ‘ ν•΄μ€λ‹ˆλ‹€.

synchronizedλ₯Ό μ‚¬μš©ν•  λ•Œ @Transactional을 μ œκ±°ν•΄μ£Όμ–΄μ•Όλ§Œ ν•˜λŠ” μ΄μœ λŠ” @Transactional을 ν†΅ν•œ 선언적 νŠΈλžœμž­μ…˜ 관리 방식은 기본적으둜 ν”„λ‘μ‹œ 방식은 AOPκ°€ 적용되기 λ•Œλ¬Έμž…λ‹ˆλ‹€. 

TransactionStatus status = transactionManager.getTransaction(..);

try {
	target.logic(); // public synchronized void decrease λ©”μ„œλ“œ μˆ˜ν–‰
    // logic μˆ˜ν–‰ 이후 νŠΈλžœμž­μ…˜ μ’…λ£Œ 전에 λ‹€λ₯Έ μ“°λ ˆλ“œκ°€ decrease에 μ ‘κ·Ό!
    transactionManager.commit(status);
} catch (Exception e) {
	transactionManager.rollback(status);
    throw new IllegalStateException(e); 
}

μœ„ μ½”λ“œλŠ” νŠΈλž™μž­μ…˜ ν”„λ‘μ‹œμ˜ κ°„λ‹¨ν•œ 예제 μ½”λ“œμž…λ‹ˆλ‹€. νŠΈλžœμž­μ…˜ ν”„λ‘μ‹œκ°€ νŠΈλžœμž­μ…˜ 처리 λ‘œμ§μ„ μˆ˜ν–‰ν•  λ•Œ μœ„ 클래슀λ₯Ό μƒˆλ‘œ λ§Œλ“€μ–΄ locic을 μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. synchronizedλ₯Ό 톡해 μ„œλΉ„μŠ€ 둜직의 decrease() λ©”μ„œλ“œλ₯Ό ν•œ μŠ€λ ˆλ“œλ§Œ μ ‘κ·Όν•  수 μžˆλ„λ‘ 막더라도 Transactional에 μ˜ν•΄ μƒˆλ‘œμš΄ ν”„λ‘μ‹œλ‘œ μˆ˜ν–‰λ˜κ²Œ λ˜μ–΄λ²„λ¦¬λ©΄ ν”„λ‘μ‹œ λ‘œμ§μ€ synchronizedκ°€ κ±Έλ¦° μƒνƒœκ°€ μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— μš°λ¦¬κ°€ μ›ν•˜λŠ” κ²°κ³Όλ₯Ό 얻지 λͺ»ν•˜κ²Œ λ©λ‹ˆλ‹€.

JAVA Sychronized의 문제점

synchronized의 μ‚¬μš©μ€ κ°„λ‹¨ν•˜μ§€λ§Œ λ‹€μŒκ³Ό 같은 λ¬Έμ œμ μ„ 가지고 μžˆμŠ΅λ‹ˆλ‹€.

  • μžλ°”μ˜ SychronizedλŠ” ν•˜λ‚˜μ˜ ν”„λ‘œμ„ΈμŠ€ μ•ˆμ—μ„œλ§Œ 보μž₯이 λ©λ‹ˆλ‹€.
  • 즉, μ„œλ²„κ°€ 1λŒ€μΌ λ•ŒλŠ” λ¬Έμ œκ°€ μ—†μ§€λ§Œ μ„œλ²„κ°€ μ—¬λŸ¬ λŒ€ 일경우 μ—¬λŸ¬ 개의 μΈμŠ€ν„΄μŠ€κ°€ μ‘΄μž¬ν•˜λŠ” 것과 λ™μΌν•˜κΈ° λ•Œλ¬Έμ— μ‹€μ§ˆμ μΈ 운영 ν™˜κ²½μ—μ„œλŠ” λ°μ΄ν„°μ˜ 정합성을 보μž₯ν•  수 μ—†μŠ΅λ‹ˆλ‹€.

 

4. DBμ—μ„œ μ œμ–΄ν•˜λŠ” 방법

이번 방법은 DateBase Lock을 μ΄μš©ν•˜μ—¬ 순차적인 μ ‘κ·ΌμœΌλ‘œ μ œμ–΄ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€. 크게 λ‹€μŒκ³Ό 같은 방법이 있으며, 쑰금 더 μƒμ„Έν•˜κ³  DBλ‹¨μ—μ„œ 직접 ν•΄κ²°ν•˜λŠ” 방법은 λ‹€μŒ 링크λ₯Ό μ°Έκ³ ν•˜λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€. 링크: MySQLμ—μ„œμ˜ Lock

  1. Pessimistic Lock(비관적 락)
  2. Optimistic Lock(낙관적 락)
  3. Named Lock(λ„€μž„λ“œ 락)

1. Pessimistic Lock

  • Pessimistic Lockμ΄λž€ μ‹€μ œλ‘œ 데이터에 Lock을 κ±Έμ–΄μ„œ 정합성을 λ§žμΆ”λŠ” λ°©λ²•μž…λ‹ˆλ‹€.
  • Exclusive Lock(배타적 잠금)을 걸게 되면 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œλŠ” Lock 이 ν•΄μ œλ˜κΈ° 전에 데이터λ₯Ό κ°€μ Έκ°ˆ 수 μ—†κ²Œ λ©λ‹ˆλ‹€.
  • μžμ› μš”μ²­μ— λ”°λ₯Έ λ™μ‹œμ„±λ¬Έμ œκ°€ λ°œμƒν•  것이라고 μ˜ˆμƒν•˜κ³  락을 κ±Έμ–΄λ²„λ¦¬λŠ” λΉ„관적 락 λ°©μ‹μž…λ‹ˆλ‹€.
  • ν•˜μ§€λ§Œ, Dead Lock(κ΅μ°©μƒνƒœ)에 빠질 μœ„ν—˜μ„±μ΄ μžˆμœΌλ―€λ‘œ μœ μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Dead Lock(κ΅μ°©μƒνƒœ)λŠ” 이전 OS 정리 λ•Œ ν¬μŠ€νŒ… ν•œ 적 μžˆμœΌλ‹ˆ μ°Έκ³ ν•˜μ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€.

 

[OS] ꡐ착 μƒνƒœ(Deadlocks)

μ„œλ‘  ν•œ μŠ€λ ˆλ“œκ°€ μžμ›μ„ μš”μ²­ν–ˆμ„ λ•Œ, κ·Έ μ‹œκ°μ— κ·Έ μžμ›μ„ μ΄μš©ν•  수 μ—†λŠ” 사황이 λ°œμƒν•  수 있고, κ·Έλ•ŒλŠ” μŠ€λ ˆλ“œκ°€ λŒ€κΈ° μƒνƒœλ‘œ λ“€μ–΄κ°‘λ‹ˆλ‹€. 이처럼 λŒ€κΈ° 쀑인 μŠ€λ ˆλ“œλ“€μ΄(그듀이 μš”μ²­ν•œ μžμ›

dkswnkk.tistory.com

비관적 락(pessimistic lock) 도식도 1

μœ„μ˜ λ„μ‹λ„μ˜ μˆ˜ν–‰ 과정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. Transaction_1μ—μ„œ table의 Id 2λ²ˆμ„ 읽음 ( name = Karol )
  2. Transaction_2μ—μ„œ table의 Id 2λ²ˆμ„ 읽음 ( name = Karol )
  3. Transaction_2μ—μ„œ table의 Id 2번의 name을 Karol2둜 λ³€κ²½ μš”μ²­ ( name = Karol )
  4. ν•˜μ§€λ§Œ Transaction 1μ—μ„œ 이미 shared Lock을 작고 있기 λ•Œλ¬Έμ— Blocking
  5. Transaction_1μ—μ„œ νŠΈλžœμž­μ…˜ ν•΄μ œ (commit)
  6. Blocking λ˜μ–΄μžˆμ—ˆλ˜ Transaction_2의 update μš”μ²­ 정상 처리

비관적 락(pessimistic lock) 도식도 2

μ½”λ“œ

public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    Optional<Stock> findById(Long id);
    
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
    @Test
    @DisplayName("Pessimistic Lock(비관적 락) μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        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();
        assertEquals(0L, stock.getQuantity());

    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

μœ„μ™€ 같이 Repositoryμ—μ„œ DB 쑰회 μ‹œμ— @Lock(value = LockModeType.PESSIMISTIC_WRITE)을 μ μš©ν•˜μ—¬ νŠΈλžœμž­μ…˜μ΄ μ‹œμž‘ν•  λ•Œ Shared/Exclusive Lock을 μ μš©ν•˜κ²Œ ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ²°κ΅­ Pesimistic Lockμ΄λž€, λ°μ΄ν„°μ—λŠ” Lock을 가진 μŠ€λ ˆλ“œλ§Œ 접근이 κ°€λŠ₯ν•˜λ„λ‘ μ œμ–΄ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

μž₯점

  • Pessimistic Lock은 λ™μ‹œμ„± 좩돌이 μž¦μ„ κ²ƒμœΌλ‘œ μ˜ˆμƒλ˜μ–΄ λ™μ‹œμ„±μ„ κ°•λ ₯ν•˜κ²Œ μ§€μΌœμ•Ό ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
  • 좩돌이 λΉˆλ²ˆν•˜κ²Œ μΌμ–΄λ‚œλ‹€λ©΄ 둀백의 횟수λ₯Ό 쀄일 수 있기 λ•Œλ¬Έμ—, Optimistic Lockλ³΄λ‹€λŠ” μ„±λŠ₯이 쒋을 수 있고, κ°€μž₯ κ°•λ ₯ν•œ 데이터 μ •ν•©μ„± 보μž₯ λ°©λ²•μž…λ‹ˆλ‹€.

단점

  • 데이터 μžμ²΄μ— Lock을 κ±ΈκΈ° λ•Œλ¬Έμ— 속도가 μƒλŒ€μ μœΌλ‘œ 느린 νŽΈμž…λ‹ˆλ‹€.
  • μ„œλ‘œ μžμ›μ΄ ν•„μš”ν•œ 경우, 락이 κ±Έλ €μžˆμœΌλ―€λ‘œ Dead Lock(κ΅μ°©μƒνƒœ)에 빠질 κ°€λŠ₯성이 μžˆμŠ΅λ‹ˆλ‹€.

 

2. Optimistic Lock

  • Optimistic Lock은 μ‹€μ œ Lock을 μ‚¬μš©ν•˜μ§€ μ•Šκ³ , λ°μ΄ν„°μ˜ Version을 μ΄μš©ν•˜μ—¬ λ°μ΄ν„°μ˜ 정합성을 μ€€μˆ˜ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.
  • λ¨Όμ € 데이터λ₯Ό μ‘°νšŒν•œ 후에 updateλ₯Ό μˆ˜ν–‰ν•  λ•Œ ν˜„μž¬ λ‚΄κ°€ μ‘°νšŒν•œ 버전이 λ§žλŠ”μ§€ ν™•μΈν•˜λ©° μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.
  • μžμ›μ— Lock을 κ±Έμ–΄μ„œ μ„ μ ν•˜μ§€ μ•Šκ³ , λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•˜λ©΄ κ·Έλ•Œ κ°€μ„œ μ²˜λ¦¬ν•˜λŠ” λ‚™κ΄€μ  락 λ°©μ‹μž…λ‹ˆλ‹€.
  • λ‚΄κ°€ μ‘°νšŒν•œ λ²„μ „μ—μ„œ μˆ˜μ •μ‚¬ν•­μ΄ 생겼을 κ²½μš°μ—λŠ” applicationμ—μ„œ λ‹€μ‹œ 쑰회 후에 μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” λ‘€λ°± μž‘μ—…μ„ μˆ˜ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€.

낙관적 락(Optimistic lock) 도식도 1

μœ„μ˜ λ„μ‹λ„μ˜ μˆ˜ν–‰ 과정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. Aκ°€ table의 Id 2λ²ˆμ„ 읽음 ( name = Karol, version = 1 )
  2. Bκ°€ table의 Id 2λ²ˆμ„ 읽음 ( name = Karol, version = 1 )
  3. Bκ°€ table의 Id 2번, version 1인 row의 κ°’ κ°±μ‹  ( name = Karol2, version = 2 ) 성곡
  4. Aκ°€ table의 Id 2번, version 1인 row의 κ°’ κ°±μ‹  ( name = Karol1, version = 2 ) μ‹€νŒ¨
  5. Id 2λ²ˆμ€ 이미 version이 2둜 μ—…λ°μ΄νŠΈλ˜μ—ˆκΈ° λ•Œλ¬Έμ— AλŠ” ν•΄λ‹Ή rowλ₯Ό κ°±μ‹ ν•˜μ§€ λͺ»ν•¨
  6. 쿼리가 μ‹€νŒ¨ν•˜λ©΄ λ‹€μ‹œ μ‘°νšŒν•˜μ—¬ 버전을 맞좘 ν›„ μ—…λ°μ΄νŠΈ 쿼리λ₯Ό λ‚ λ¦¬λŠ” 과정을 반볡

낙관적 락(Optimistic lock) 도식도 2

μœ„ flowλ₯Ό ν†΅ν•΄μ„œ κ°™μ€ row에 λŒ€ν•΄μ„œ 각기 λ‹€λ₯Έ 2개의 μˆ˜μ • μš”μ²­μ΄ μžˆμ—ˆμ§€λ§Œ 1κ°œκ°€ μ—…λ°μ΄νŠΈλ¨μ— 따라 version이 λ³€κ²½λ˜μ—ˆκΈ° λ•Œλ¬Έμ— λ’€μ˜ μˆ˜μ • μš”μ²­μ€ λ°˜μ˜λ˜μ§€ μ•Šκ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ 낙관적락은 versionκ³Ό 같은 λ³„λ„μ˜ μ»¬λŸΌμ„ μΆ”κ°€ν•˜μ—¬ 좩돌적인 μ—…λ°μ΄νŠΈλ₯Ό λ§‰μŠ΅λ‹ˆλ‹€. version 뿐만 μ•„λ‹ˆλΌ hashcode λ˜λŠ” timestampλ₯Ό μ΄μš©ν•˜κΈ°λ„ ν•©λ‹ˆλ‹€.

μ½”λ“œ

Pessimistic Lockκ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ DB 쑰회 μ‹œμ— @Lock(value = LockModeType.OPTIMISTIC)을 λͺ…μ‹œν•˜μ—¬ μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.OPTIMISTIC)
    Optional<Stock> findById(Long id);
    
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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 -= quantity;
    }
    
}

Entityμ—λŠ” @Version을 톡해 version μ»¬λŸΌμ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}

Service둜직의 κ²½μš°μ—λŠ” 기쑴의 둜직과 차이가 μ—†κ³ , μ§€μ†μ μœΌλ‘œ DB 변경을 μž¬μ‹œλ„ν•˜λŠ” λ‘œμ§μ„ κ΅¬ν˜„ν•΄μ•Ό ν–ˆκΈ° λ•Œλ¬Έμ— λ³„λ„μ˜ Helper μœ ν‹Έλ¦¬ν‹°μ™€ 같은 Facadeλ₯Ό μ•„λž˜μ™€ 같이 λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. 

@Component
@RequiredArgsConstructor
public class OptimisticLockStockFacade {

    private final StockService stockService;
	
 	/**
 	* Lock을 μž‘μ§€ μ•ŠμœΌλ―€λ‘œ, Pessimistic Lock(비관적 락)보닀 μ„±λŠ₯상 이점이 μžˆμ„ 수 μžˆλ‹€.
 	* ν•˜μ§€λ§Œ, μ—…λ°μ΄νŠΈκ°€ μ‹€νŒ¨ν–ˆμ„ 경우 μž¬μ‹œλ„ 둜직이 κ°œλ°œμžκ°€ 직접 μž‘μ„±μ„ ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.
 	* λ˜ν•œ 좩돌이 λΉˆλ²ˆν•˜κ²Œ μΌμ–΄λ‚œλ‹€λ©΄, Pessimistic Lock이 μ„±λŠ₯상 이점이 더 μžˆμ„ 수 μžˆλ‹€.
 	*/
    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (true) {
            try {
                stockService.decrease(id, quantity);
                break;
            } catch (Exception e) {
                Thread.sleep(1);
            }
        }
    }
}

Optimistic Lockμ—μ„œ Version을 ν™•μΈν•˜λŠ”λ°, λ§Œμ•½ DB λ³€κ²½ νŠΈλžœμž­μ…˜μ„ λ§Œλ“€κ³ μž ν•˜λŠ” ν˜„μž¬ μŠ€λ ˆλ“œκ°€ 이전 Version을 가지고 μžˆλ‹€λ©΄ νŠΈλžœμž­μ…˜μ„ 보내지 λͺ»ν•˜κ³  1msλ™μ•ˆ 기닀리도둝 μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€.

    @Test
    @DisplayName("Optimistic Lock(낙관적 락) μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

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

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());
    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

μž₯점

  • 좩돌이 자주 μΌμ–΄λ‚˜μ§€ μ•ŠλŠ”λ‹€λŠ” κ°€μ • ν•˜μ—, λ³„λ„μ˜ Lock을 걸지 μ•ŠμœΌλ―€λ‘œ Pessimistic Lockλ³΄λ‹€λŠ” μ„±λŠ₯적 이점을 κ°€μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

단점

  • μ—…λ°μ΄νŠΈκ°€ μ‹€νŒ¨ν–ˆμ„ μ‹œ, μž¬μ‹œλ„ λ‘œμ§μ„ κ°œλ°œμžκ°€ 직접 μž‘μ„±ν•΄ μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.
  • 좩돌이 λΉˆλ²ˆν•˜κ²Œ μΌμ–΄λ‚œλ‹€λ©΄, Roll Back 처리λ₯Ό ν•΄μ£Όμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ—, Pessimistic Lock이 더 μ„±λŠ₯이 쒋을 수 μžˆμŠ΅λ‹ˆλ‹€.

 

3. Named Lock

  • Named Lock은 이름을 가진 Metadata Lockμž…λ‹ˆλ‹€.
  • 이름을 가진 Lock을 νšλ“ν•œ ν›„, 해지될 λ•ŒκΉŒμ§€ λ‹€λ₯Έ μ„Έμ…˜μ€ 이 Lock을 νšλ“ν•  수 μ—†κ²Œ λ©λ‹ˆλ‹€.
  • μ£Όμ˜ν•  점은, νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλ  λ•Œ Lock이 μžλ™μœΌλ‘œ ν•΄μ§€λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, λ³„λ„λ‘œ ν•΄μ§€ν•΄μ£Όκ±°λ‚˜ μ„ μ μ‹œκ°„μ΄ λλ‚˜μ•Ό ν•΄μ§€λ©λ‹ˆλ‹€.
  • Mysqlμ—μ„œλŠ” getLock( )을 톡해 νšλ“€ / releaseLock()으둜 해지할 수 μžˆμŠ΅λ‹ˆλ‹€.

Named Lock 도식도

μœ„μ˜ λ„μ‹λ„μ˜ μˆ˜ν–‰ 과정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. Named Lock은 Stock에 락을 걸지 μ•Šκ³ , λ³„λ„μ˜ 곡간에 Lock을 건닀.
  2. session-1 이 1μ΄λΌλŠ” μ΄λ¦„μœΌλ‘œ Lock을 건닀면, session 1 이 1을 ν•΄μ§€ν•œ 후에 Lock을 얻을 수 μžˆλ‹€.

μ£Όμ˜ν•  점은 Named Lock을 ν™œμš©ν•  λ•Œ λ°μ΄ν„°μ†ŒμŠ€λ₯Ό λΆ„λ¦¬ν•˜μ§€ μ•Šκ³ , ν•˜λ‚˜λ‘œ μ‚¬μš©ν•˜κ²Œ 되면 connection pool이 λΆ€μ‘±ν•΄μ§€λŠ” ν˜„μƒμ΄ λ°œμƒν•΄ Lock을 μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” λ‹€λ₯Έ μ„œλΉ„μŠ€κΉŒμ§€ 영ν–₯을 끼칠 수 μžˆλ‹€λŠ” λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

Named Lock을 ν™œμš©ν•˜λ©΄ λΆ„μ‚° 락을 κ΅¬ν˜„ν•  수 있고, Pessmistic  Lock의 경우 νƒ€μž„μ•„μ›ƒμ„ κ΅¬ν˜„ν•˜κΈ° κΉŒλ‹€λ‘­μ§€λ§Œ, Named Lock은 μ†μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ νŠΈλžœμž­μ…˜ μ’…λ£Œ μ‹œμ— Lock ν•΄μ œμ™€ 데이터 μ†ŒμŠ€ 뢄리 μ‹œ μ„Έμ…˜ 관리λ₯Ό μˆ˜λ™μœΌλ‘œ μ§„ν–‰λ˜μ–΄μ•Ό ν•œλ‹€λŠ” λΆˆνŽΈν•œ 점이 μžˆμŠ΅λ‹ˆλ‹€.

μ½”λ“œ

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)", nativeQuery = true)
    void releaseLock(String key);
    
}
@Component
@RequiredArgsConstructor
public class NamedLockFacade {

    private final LockRepository lockRepository;
    private final StockService stockService;
	
    /**
    * Named Lock은 주둜 λΆ„μ‚° 락을 κ΅¬ν˜„ν•  λ•Œ μ‚¬μš©ν•œλ‹€.
    * Pessimistic Lock은 νƒ€μž„μ•„μ›ƒμ„ κ΅¬ν˜„ν•˜κΈ° ꡉμž₯히 κΉŒλ‹€λ‘­μ§€λ§Œ, Named Lock은 μ†μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆλ‹€.
    * 이외에도 데이터 μ‚½μž…μ‹œμ— 정합성을 λ§žμΆ°μ•Ό ν•˜λŠ” κ²½μš°μ—λ„ μ‚¬μš©ν•  수 μžˆλ‹€.
    * ν•˜μ§€λ§Œ 이 방법은 νŠΈλžœμž­μ…˜ μ’…λ£Œμ‹œμ— 락 ν•΄μ œμ™€ μ„Έμ…˜ 관리λ₯Ό 직접 잘 ν•΄μ€˜μ•Όν•˜λ―€λ‘œ μ£Όμ˜ν•΄μ„œ μ‚¬μš©ν•΄μ•Όν•˜κ³ , μ‚¬μš©ν•  λ•ŒλŠ” κ΅¬ν˜„λ°©λ²•μ΄ λ³΅μž‘ν•  수 μžˆλ‹€.
    */
    @Transactional
    public void decrease(Long id, Long quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrease(id, quantity);
        }finally {
            // Lock ν•΄μ œ
            lockRepository.releaseLock(id.toString());
        }
    }
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    // λΆ€λͺ¨μ˜ νŠΈλžœμž­μ…˜κ³Ό λ³„λ„λ‘œ μ‹€ν–‰λ˜μ–΄μ•Ό 함
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}

μ—¬κΈ°μ„œ μ€‘μš” 둜직의 μ„€λͺ…은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • StockServiceλŠ” λΆ€λͺ¨μ˜ νŠΈλžœμž­μ…˜κ³Ό λ³„λ„λ‘œ μ‹€ν–‰λ˜μ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— propergation을 λ³„λ„λ‘œ 생성해 μ€λ‹ˆλ‹€
  • λΆ€λͺ¨μ˜ νŠΈλžœμž­μ…˜κ³Ό λ™μΌν•œ λ²”μœ„λ‘œ 묢인닀면 Synchronized와 같은 문제인 DataBase에 Commit 되기 전에 Lock이 ν’€λ¦¬λŠ” ν˜„μƒμ΄ λ°œμƒν•©λ‹ˆλ‹€.
  • κ·Έλ ‡κΈ° λ•Œλ¬Έμ— λ³„λ„μ˜ νŠΈλžœμž­μ…˜μœΌλ‘œ λΆ„λ¦¬ν•΄μ„œ DataBase에 μ •μƒμ μœΌλ‘œ Commit이 된 후에 락을 ν•΄μ œν•΄ 주도둝 ν•©λ‹ˆλ‹€.
  • 핡심은 Lock을 ν•΄μ œν•˜κΈ° 전에 DataBase에 Commit이 λ˜λ„λ‘ ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

그리고 Connection Pool의 수λ₯Ό λŠ˜λ €μ€λ‹ˆλ‹€.

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 	// here
    @Test
    @DisplayName("Named Lock μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

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

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());
    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

μž₯점

  • Named Lock 은 주둜 뢄산락을 κ΅¬ν˜„ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
  • Pessimistic 락은 timeout을 κ΅¬ν˜„ν•˜κΈ° ꡉμž₯히 νž˜λ“€μ§€λ§Œ, Named Lock은 비ꡐ적 μ†μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

단점

  • Named Lock 은 νŠΈλžœμž­μ…˜ μ’…λ£Œ μ‹œμ—, Lock ν•΄μ œμ™€ μ„Έμ…˜κ΄€λ¦¬λ₯Ό μž˜ν•΄μ£Όμ–΄μ•Ό ν•˜λ―€λ‘œ μ£Όμ˜ν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • μ‹€μ œ 업무 ν™˜κ²½μ—μ„œλŠ” κ΅¬ν˜„λ°©λ²•μ΄ λ³΅μž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

5. Redisλ₯Ό ν™œμš©ν•˜λŠ” 방법

Redisλž€ key-value ꡬ쑰의 λΉ„μ •ν˜• 데이터λ₯Ό μ €μž₯ν•˜κ³  κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ μ˜€ν”ˆ μ†ŒμŠ€ 기반의 λΉ„ κ΄€κ³„ν˜• 인메λͺ¨λ¦¬ DBMSμž…λ‹ˆλ‹€. Redis의 λ‹€μ–‘ν•œ νŠΉμ§• μ€‘μ—μ„œλ„ Single Threaded ν•œ νŠΉμ§• 즉, ν•œ λ²ˆμ— ν•˜λ‚˜μ˜ λͺ…λ Ήλ§Œ μ²˜λ¦¬ν•  수 μžˆλŠ” νŠΉμ§• λ•Œλ¬Έμ— λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜λŠ”λ° 많이 μ‚¬μš©λ©λ‹ˆλ‹€.

Java의 Redis ClientλŠ” μ•„λž˜μ™€ 같이 크게 μ„Έ 가지가 μžˆμŠ΅λ‹ˆλ‹€.

이 μ€‘μ—μ„œ JedisλŠ” μ„±λŠ₯이 많이 쒋지 μ•ŠκΈ° λ•Œλ¬Έμ— Lettuce와 Redisson을 λΉ„κ΅ν•΄μ„œ μ •λ¦¬ν•˜κ² μŠ΅λ‹ˆλ‹€. Jedisμ™€μ˜ μ„±λŠ₯ λΉ„κ΅λŠ” μ‘°μ‘Έλ‘λ‹˜μ˜ λΈ”λ‘œκ·Έμ— μžμ„Ένžˆ μ„€λͺ…λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. 링크: https://jojoldu.tistory.com/418

1. Lettuce

  • Setnx λͺ…λ Ήμ–΄λ₯Ό ν™œμš©ν•˜μ—¬ 뢄산락을 κ΅¬ν˜„ (Set if not Exist - key:valueλ₯Ό Set ν•  λ–„. 기쑴의 값이 없을 λ•Œλ§Œ Set ν•˜λŠ” λͺ…λ Ήμ–΄)
  • SetnxλŠ” Spin Lockλ°©μ‹μ΄λ―€λ‘œ retry λ‘œμ§μ„ κ°œλ°œμžκ°€ μž‘μ„±ν•΄ μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.
  • Spin Lock μ΄λž€, Lock을 νšλ“ν•˜λ €λŠ” μŠ€λ ˆλ“œκ°€ Lock을 νšλ“ν•  수 μžˆλŠ”μ§€ ν™•μΈν•˜λ©΄μ„œ 반볡적으둜 μ‹œλ„ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

락을 νšλ“ν•œλ‹€λŠ” 것은 “락이 μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•œλ‹€”, “μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ 락을 νšλ“ν•œλ‹€” λ‘ 연산이 atomic ν•˜κ²Œ 이루어져야 ν•©λ‹ˆλ‹€. Redis에 κΈ°λ°˜ν•œ Lettuce 방식은 “값이 μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ μ„ΈνŒ…ν•œλ‹€”λΌλŠ” setnx(set when not exists) λͺ…λ Ήμ–΄λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€. 이 setnxλ₯Ό μ΄μš©ν•˜μ—¬ λ ˆλ””μŠ€μ— 값이 μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ μ„ΈνŒ…ν•˜κ²Œ ν•˜κ³ , 값이 μ„ΈνŒ…λ˜μ—ˆλŠ”μ§€ μ—¬λΆ€λ₯Ό 리턴 κ°’μœΌλ‘œ λ°›μ•„ 락을 νšλ“ν•˜λŠ” 데에 μ„±κ³΅ν–ˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.

Spin Lock κ³Όμ •

Named Lockκ³Ό 달리 Redisλ₯Ό μ‚¬μš©ν•˜λ©΄ νŠΈλžœμž­μ…˜μ— 따라 λŒ€μ‘λ˜λŠ” ν˜„μž¬ νŠΈλžœμž­μ…˜ ν’€ μ„Έμ…˜ 관리λ₯Ό ν•˜μ§€ μ•Šμ•„λ„ λ˜λ―€λ‘œ κ΅¬ν˜„μ΄ νŽΈλ¦¬ν•©λ‹ˆλ‹€. μ•žμ„œ λ§ν–ˆλ“― Spin Lock λ°©μ‹μ΄λ―€λ‘œ Sleep Time이 μ μ„μˆ˜λ‘ Redis에 λΆ€ν•˜λ₯Ό 쀄 수 μžˆμ–΄μ„œ thread busy waiting의 μš”μ²­ κ°„μ˜ μ‹œκ°„μ„ 적절히 μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.

μ½”λ“œ

dependencies {
    //redis μ˜μ‘΄μ„± μΆ”κ°€
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
@Component
@RequiredArgsConstructor
public class RedisLockRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public Boolean lock(final Long key) {
        return redisTemplate
            .opsForValue()
            .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();
    }
    
}
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade  {

    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;
	
   /**
   * κ΅¬ν˜„μ΄ κ°„λ‹¨ν•˜λ‹€.
   * Spring Data Redisλ₯Ό μ΄μš©ν•˜λ©΄ Lettuceκ°€ 기본이기 λ•Œλ¬Έμ— λ³„λ„μ˜ 라이브러리λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.
   * Spin Lock 방식이기 λ•Œλ¬Έμ— λ™μ‹œμ— λ§Žμ€ μŠ€λ ˆλ“œκ°€ Lock νšλ“ λŒ€κΈ° μƒνƒœλΌλ©΄ Redis에 λΆ€ν•˜κ°€ 갈 수 μžˆλ‹€.
   * μ‹€λ¬΄μ—μ„œλŠ” μž¬μ‹œλ„κ°€ ν•„μš”ν•œ Lock의 κ²½μš°μ—λŠ” Redission을 ν™œμš©ν•˜κ³ , 그렇지 μ•Šμ„ κ²½μš°μ—λŠ” Lettuce을 ν™œμš©ν•œλ‹€.
   * μž¬μ‹œλ„κ°€ ν•„μš”ν•œ 경우?: μ„ μ°©μˆœ 100λͺ… κΉŒμ§€ λ¬Όν’ˆμ„ ꡬ맀할 수 μžˆμ„ 경우
   * μž¬μ‹œλ„κ°€ ν•„μš”ν•˜μ§€ μ•Šμ€ 경우?: μ„ μ°©μˆœ ν•œλͺ…λ§Œ κ°€λŠ₯, Lock νšλ“ μž¬μ‹œλ„ ν•  ν•„μš”κ°€ μ—†μŒ
   */
    public void decrease(final Long key, final Long quantity) throws InterruptedException {
        while (!redisLockRepository.lock(key)) {
            Thread.sleep(50);
        }

        //lock νšλ“ μ„±κ³΅μ‹œ
        try{
            stockService.decrease(key,quantity);
        }finally {
         //락 ν•΄μ œ
            redisLockRepository.unlock(key);
        }
    }
    
}

Spin Lockλ°©μ‹μœΌλ‘œ Lock μ–»κΈ°λ₯Ό μ‹œλ„ν•˜κ³ , Lock을 얻은 ν›„, 재고 κ°μ†Œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€. κ·Έ ν›„ Lock을 ν•΄μ œν•˜λŠ” λ°©λ²•μœΌλ‘œ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

    @Test
    @DisplayName("Lettuce Lock μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                try {
                    lettuceLockStockFacade.decrease(1L, 1L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());

    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

Spin Lock λ°©μ‹μ—λŠ” λ‹€μŒκ³Ό 같은 μ—¬λŸ¬ 가지 문제점이 μžˆμŠ΅λ‹ˆλ‹€.(https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html)

1. Lock의 νƒ€μž„μ•„μ›ƒμ΄ μ§€μ •λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

μœ„μ˜ μ½”λ“œμ™€ 같이 μŠ€ν•€ 락을 κ΅¬ν˜„ν–ˆμ„μ‹œμ— 락을 νšλ“ν•˜μ§€ λͺ»ν•˜λ©΄ λ¬΄ν•œ 루프λ₯Ό 돌게 λ©λ‹ˆλ‹€. λ§Œμ•½ νŠΉμ •ν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ tryLock을 μ„±κ³΅ν–ˆλŠ”λ° λΆˆμš΄ν•˜κ²Œλ„ μ–΄λ–€ 였λ₯˜ λ•Œλ¬Έμ— μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ’…λ£Œλ˜μ–΄λ²„λ¦¬λ©΄ μ–΄λ–»κ²Œ λ κΉŒμš”? λ‹€λ₯Έ λͺ¨λ“  μ• ν”Œλ¦¬μΌ€μ΄μ…˜κΉŒμ§€ μ˜μ›νžˆ 락을 νšλ“ν•˜μ§€ λͺ»ν•œ 채 락이 ν•΄μ œλ˜κΈ°λ§Œμ„ κΈ°λ‹€λ¦¬λŠ” λ¬΄ν•œμ • λŒ€κΈ°μƒνƒœκ°€ λ˜μ–΄ 전체 μ„œλΉ„μŠ€μ˜ μž₯μ• κ°€ λ°œμƒν•˜κ²Œ 될 κ²ƒμž…λ‹ˆλ‹€.

κ·Έλž˜μ„œ 일반적인 둜컬 μŠ€ν•€ λ½κ³ΌλŠ” λ‹€λ₯΄κ²Œ 일정 μ‹œκ°„μ΄ μ§€λ‚˜λ©΄ 락이 λ§Œλ£Œλ˜λ„λ‘ κ΅¬ν˜„ν•΄μ•Ό ν•©λ‹ˆλ‹€. 그러렀면 expire time을 μ„€μ •ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μœ„μ˜ μ½”λ“œμ—μ„œλŠ” “락을 μ‚¬μš© 쀑인지 확인”, “락을 νšλ“” μ—°μ‚°μ„ ν•˜λ‚˜λ‘œ λ¬ΆκΈ° μœ„ν•΄ setnx λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. 이 λͺ…λ Ήμ–΄λŠ” expire time을 지정할 수 없기에 이 문제λ₯Ό ν•΄κ²°ν•˜κΈ°κ°€ νž˜λ“­λ‹ˆλ‹€.

λ˜ν•œ λ¬΄ν•œμ •μœΌλ‘œ 락의 νšλ“μ„ μ‹œλ„ν•œλ‹€λ©΄ λ¬Έμ œκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€. λ§Œμ•½ 연산이 였래 걸릴 경우 λŒ€λΆ€λΆ„μ˜ μŠ€λ ˆλ“œκ°€ 락을 λŒ€κΈ°ν•˜λŠ” μƒνƒœκ°€ λ˜μ–΄ ν΄λΌμ΄μ–ΈνŠΈμ— μ‘λ‹΅ν•˜λŠ” 속도가 λŠ¦μ–΄μ§€κ³ , λ™μ‹œμ— λ ˆλ””μŠ€μ— μ—„μ²­λ‚œ νŠΈλž˜ν”½μ„ 보낼 수 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€. κ·Έλž˜μ„œ 락을 νšλ“ν•˜λŠ” μ΅œλŒ€ ν—ˆμš©μ‹œκ°„μ„ μ •ν•΄μ£Όκ±°λ‚˜, μ΅œλŒ€ ν—ˆμš© 횟수λ₯Ό μ •ν•΄μ£ΌλŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. λ§Œμ•½ 락을 νšλ“ν•˜λŠ” 데에 μ‹€νŒ¨ν•œλ‹€λ©΄ 연산을 μˆ˜ν–‰ν•  수 μ—†λŠ” μƒνƒœμ΄κΈ°μ— Exception을 λ˜μ§‘λ‹ˆλ‹€.

2. Redis에 λ§Žμ€ λΆ€ν•˜λ₯Ό κ°€ν•˜κ²Œ λ©λ‹ˆλ‹€.

μœ„μ˜ μ½”λ“œλŠ” μŠ€ν•€ 락을 μ‚¬μš©ν–ˆμ§€λ§Œ 사싀 μŠ€ν•€ 락을 μ‚¬μš©ν•˜λ©΄ λ ˆλ””μŠ€μ— μ—„μ²­λ‚œ 뢀담을 주게 λ©λ‹ˆλ‹€. μŠ€ν•€ 락은 μ§€μ†μ μœΌλ‘œ 락의 νšλ“μ„ μ‹œλ„ν•˜λŠ” μž‘μ—…μ΄κΈ° λ•Œλ¬Έμ— λ ˆλ””μŠ€μ— 계속 μš”μ²­μ„ λ³΄λ‚΄κ²Œ 되고 λ ˆλ””μŠ€λŠ” 이런 νŠΈλž˜ν”½μ„ μ²˜λ¦¬ν•˜λŠλΌ 뢀담을 λ°›κ²Œ λ©λ‹ˆλ‹€.

μŠ€ν•€ 락을 μ‚¬μš©ν•˜λ©΄μ„œ λ ˆλ””μŠ€μ— 뢀담을 덜 μ£ΌκΈ° μœ„ν•΄ 50ms만큼 sleep ν•˜λ©΄μ„œ tryLock을 μˆ˜ν–‰ν•˜λ„λ‘ ν–ˆμ§€λ§Œ, 이 λ˜ν•œ 50msλ§ˆλ‹€ 계속 λ ˆλ””μŠ€μ— μš”μ²­μ„ λ³΄λ‚΄λŠ” κ²ƒμ΄λ―€λ‘œ μž‘μ—…μ΄ 였래 걸릴수둝, μš”μ²­ μˆ˜κ°€ λ§Žμ„μˆ˜λ‘ 더 큰 λΆ€ν•˜λ₯Ό κ°€ν•˜κ²Œ λ©λ‹ˆλ‹€.

λ§Œμ•½ 300msκ°€ κ±Έλ¦¬λŠ” λ™κΈ°ν™”λœ μž‘μ—…μ— λ™μ‹œμ— 100개의 μš”μ²­μ΄ μ™”λ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€. (λΆ„μ‚° λ½μ΄λ―€λ‘œ μ„œλ²„μ˜ λŒ€μˆ˜λŠ” λ¬΄κ΄€ν•©λ‹ˆλ‹€.)

처음으둜 락을 νšλ“ν•˜λŠ” 데 μ„±κ³΅ν•œ 1개의 μš”μ²­μ„ μ œμ™Έν•˜κ³ , λ‚˜λ¨Έμ§€ 99개의 μš”μ²­μ€ μž‘μ—…μ΄ μ™„λ£Œλ˜λŠ” 300ms λ™μ•ˆ 무렀 λ ˆλ””μŠ€μ— 594회의 락 νšλ“ μš”μ²­μ„ ν•˜κ²Œ λ©λ‹ˆλ‹€. 즉 1초 λ™μ•ˆ μ•½ 2000νšŒλΌλŠ” λ§Žμ€ μš”μ²­μ„ λ ˆλ””μŠ€μ— λ³΄λ‚΄κ²Œ λ©λ‹ˆλ‹€.

λ˜ν•œ μΌνšŒμ„±μ΄ μ•„λ‹ˆλΌ λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ  λ•ŒκΉŒμ§€ μ§€μ†μ μœΌλ‘œ λ ˆλ””μŠ€μ— λΆ€ν•˜λ₯Ό κ°€ν•˜κΈ° λ•Œλ¬Έμ— μš”μ²­μ΄ μ§€μ†μ μœΌλ‘œ λ“€μ–΄μ˜€λŠ” ν™˜κ²½μ΄λΌλ©΄ μ΄λŸ¬ν•œ λΉ„νš¨μœ¨μ„±μ€ λ”μš± μ»€μ§‘λ‹ˆλ‹€.

λ§Œμ•½ λ ˆλ””μŠ€μ— 뢀담을 덜 μ£ΌκΈ° μœ„ν•΄ sleep μ‹œκ°„μ„ 300ms둜 λŠ˜λ¦°λ‹€λ©΄ μ–΄λ–¨κΉŒμš”? 50msκ°€ κ±Έλ¦¬λŠ” μž‘μ—…μ— 이 동기화λ₯Ό μ μš©ν•˜λ©΄ 락을 νšλ“ν•˜μ§€ λͺ»ν•  경우 50ms κ±Έλ¦¬λŠ” μž‘μ—…μ„ ν•˜κΈ° μœ„ν•΄ 300msλ₯Ό λŒ€κΈ°ν•΄μ•Ό ν•˜λŠ” λ‹€λ₯Έ λΉ„νš¨μœ¨μ μΈ 상황이 μƒκΈ°κ²Œ λ©λ‹ˆλ‹€.

 

2. Redisson

  • Pub-sub 기반으둜 Lock κ΅¬ν˜„ 제곡
  • Pub-Sub λ°©μ‹μ΄λž€, 채널을 ν•˜λ‚˜ λ§Œλ“€κ³ , 락을 점유 쀑인 μŠ€λ ˆλ“œκ°€, Lock을 ν•΄μ œν–ˆμŒμ„, λŒ€κΈ° 쀑인 μŠ€λ ˆλ“œμ—κ²Œ μ•Œλ €μ£Όλ©΄ λŒ€κΈ° 쀑인 μŠ€λ ˆλ“œκ°€ Lock 점유λ₯Ό μ‹œλ„ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • 이 방식은, Lettuce와 λ‹€λ₯΄κ²Œ λŒ€λΆ€λΆ„ λ³„λ„μ˜ Retry 방식을 μž‘μ„±ν•˜μ§€ μ•Šμ•„λ„ λ©λ‹ˆλ‹€.

Redisson 점유 κ³Όμ •

μ½”λ“œ

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    // redisson μ˜μ‘΄μ„± μΆ”κ°€
    implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.19.0'
}
@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {

    private final RedissonClient redissonClient;
    private final StockService stockService;
	
   /**
   * Lock νšλ“ μž¬μ‹œλ„λ₯Ό 기본으둜 μ œκ³΅ν•œλ‹€.
   * pub-sub λ°©μ‹μœΌλ‘œ κ΅¬ν˜„μ΄ λ˜μ–΄μžˆκΈ° λ•Œλ¬Έμ— Lettuce와 λΉ„κ΅ν–ˆμ„ λ•Œ Redis에 λΆ€ν•˜κ°€ 덜 κ°„λ‹€.
   * λ³„λ„μ˜ 라이브러리λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.
   * Lock을 라이브러리 μ°¨μ›μ—μ„œ μ œκ³΅ν•΄μ£ΌκΈ° λ•Œλ¬Έμ— μ‚¬μš©λ²•μ„ 곡뢀해야 ν•œλ‹€.
   * μ‹€λ¬΄μ—μ„œλŠ” μž¬μ‹œλ„κ°€ ν•„μš”ν•œ Lock의 κ²½μš°μ—λŠ” Redission을 ν™œμš©ν•˜κ³ , 그렇지 μ•Šμ„ κ²½μš°μ—λŠ” Lettuce을 ν™œμš©ν•œλ‹€.
   * μž¬μ‹œλ„κ°€ ν•„μš”ν•œ 경우?: μ„ μ°©μˆœ 100λͺ… κΉŒμ§€ λ¬Όν’ˆμ„ ꡬ맀할 수 μžˆμ„ 경우
   * μž¬μ‹œλ„κ°€ ν•„μš”ν•˜μ§€ μ•Šμ€ 경우?: μ„ μ°©μˆœ ν•œλͺ…λ§Œ κ°€λŠ₯, Lock νšλ“ μž¬μ‹œλ„ ν•  ν•„μš”κ°€ μ—†μŒ
   */
    public void decrease(final Long key, final Long quantity) {
    
        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();
        }
    }
    
}

Redisson은 Lettuce와 달리 λ³„λ„μ˜ μΈν„°νŽ˜μ΄μŠ€μ΄κΈ° λ•Œλ¬Έμ— gradle 의쑴 νŒ¨ν‚€μ§€ μ„€μΉ˜ 및 λ³„λ„μ˜ Facade μž‘μ„±μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

    @Test
    @DisplayName("Redisson Lock μ‚¬μš©")
    public void λ™μ‹œμ—_100개_μš”μ²­() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

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

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());

    }

ν…ŒμŠ€νŠΈ 둜직 μˆ˜ν–‰ κ²°κ³Ό

μ΄μ œλŠ” μ˜€ν”ˆμ†ŒμŠ€ λ ˆλ””μŠ€ ν΄λΌμ΄μ–ΈνŠΈμΈ Redisson이 λΆ„μ‚° 락을 μ–΄λ–»κ²Œ μ„€κ³„ν–ˆλŠ”μ§€ μ†Œκ°œν•˜λ©° μ–΄λ–»κ²Œ 이 λ¬Έμ œμ λ“€μ„ ν•΄κ²°ν–ˆκ³ , 보닀 λΉ λ₯Έ μ„±λŠ₯을 λ‚΄κ²Œ λ˜μ—ˆλŠ”μ§€ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.

Redisson은 Lettuce와 λΉ„μŠ·ν•˜κ²Œ Nettyλ₯Ό μ‚¬μš©ν•˜μ—¬ non-blocking I/Oλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. Redisson의 νŠΉμ΄ν•œ 점은 직접 λ ˆλ””μŠ€μ˜ λͺ…λ Ήμ–΄λ₯Ό μ œκ³΅ν•˜μ§€ μ•Šκ³ , Bucketμ΄λ‚˜ Map같은 μžλ£Œκ΅¬μ‘°λ‚˜ Lock κ°™μ€ νŠΉμ •ν•œ κ΅¬ν˜„μ²΄μ˜ ν˜•νƒœλ‘œ μ œκ³΅ν•œλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.

1. Lock에 νƒ€μž„μ•„μ›ƒμ΄ κ΅¬ν˜„λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

Redisson은 tryLock λ©”μ†Œλ“œμ— νƒ€μž„μ•„μ›ƒμ„ λͺ…μ‹œν•˜λ„λ‘ λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€. 첫 번째 νŒŒλΌλ―Έν„°λŠ” 락 νšλ“μ„ λŒ€κΈ°ν•  νƒ€μž„μ•„μ›ƒμ΄κ³ , 두 번째 νŒŒλΌλ―Έν„°λŠ” 락이 λ§Œλ£Œλ˜λŠ” μ‹œκ°„μž…λ‹ˆλ‹€.

첫 번째 νŒŒλΌλ―Έν„°λ§ŒνΌμ˜ μ‹œκ°„μ΄ μ§€λ‚˜λ©΄ falseκ°€ λ°˜ν™˜λ˜λ©° 락 νšλ“μ— μ‹€νŒ¨ν–ˆλ‹€κ³  μ•Œλ €μ€λ‹ˆλ‹€. 그리고 두 번째 νŒŒλΌλ―Έν„°λ§ŒνΌμ˜ μ‹œκ°„μ΄ μ§€λ‚˜λ©΄ 락이 λ§Œλ£Œλ˜μ–΄ 사라지기 λ•Œλ¬Έμ— μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ 락을 ν•΄μ œν•΄μ£Όμ§€ μ•Šλ”λΌλ„ λ‹€λ₯Έ μŠ€λ ˆλ“œ ν˜Ήμ€ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ 락을 νšλ“ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

이둜 인해 락이 ν•΄μ œλ˜μ§€ μ•ŠλŠ” 문제둜 λ¬΄ν•œ 루프에 빠질 μœ„ν—˜μ΄ μ‚¬λΌμ§‘λ‹ˆλ‹€.

// RedissonLock의 tryLock λ©”μ†Œλ“œ μ‹œκ·Έλ‹ˆμ³
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException

2. μŠ€ν•€ 락을 μ‚¬μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

Redisson은 기본적으둜 μŠ€ν•€ 락을 μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— λ ˆλ””μŠ€μ— 뢀담을 주지 μ•ŠμŠ΅λ‹ˆλ‹€. 그럼 μ–΄λ–»κ²Œ 락의 νšλ“ κ°€λŠ₯μ—¬λΆ€λ₯Ό νŒλ‹¨ν• κΉŒμš”?

Redisson은 pubsub κΈ°λŠ₯을 μ‚¬μš©ν•˜μ—¬ μŠ€ν•€ 락이 λ ˆλ””μŠ€μ— μ£ΌλŠ” μ—„μ²­λ‚œ νŠΈλž˜ν”½μ„ μ€„μ˜€μŠ΅λ‹ˆλ‹€. 락이 ν•΄μ œλ  λ•Œλ§ˆλ‹€ subscribe ν•˜λŠ” ν΄λΌμ΄μ–ΈνŠΈλ“€μ—κ²Œ “λ„ˆλ„€λŠ” 이제 락 νšλ“μ„ μ‹œλ„ν•΄λ„ λœλ‹€”λΌλŠ” μ•Œλ¦Όμ„ μ£Όμ–΄μ„œ 일일이 λ ˆλ””μŠ€μ— μš”μ²­μ„ 보내 락의 νšλ“κ°€λŠ₯μ—¬λΆ€λ₯Ό μ²΄ν¬ν•˜μ§€ μ•Šμ•„λ„ λ˜λ„λ‘ κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ Redisson은 μ΅œλŒ€ν•œ λ ˆλ””μŠ€μ™€ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— λΆ€ν•˜λ₯Ό 주지 μ•Šλ„λ‘ μ‹ κ²½ μ“΄ λͺ¨μŠ΅μ΄ λ³΄μž…λ‹ˆλ‹€. μ•„λž˜λŠ” Redisson의 Lock νšλ“ ν”„λ‘œμ„ΈμŠ€μž…λ‹ˆλ‹€.

  1. λŒ€κΈ° μ—†λŠ”tryLock μ˜€νΌλ ˆμ΄μ…˜μ„ ν•˜μ—¬ 락 νšλ“μ— μ„±κ³΅ν•˜λ©΄ trueλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. μ΄λŠ” 경합이 없을 λ•Œ μ•„λ¬΄λŸ° μ˜€λ²„ν—€λ“œ 없이 락을 νšλ“ν•  수 μžˆλ„λ‘ ν•΄μ€λ‹ˆλ‹€.
  2. pubsub을 μ΄μš©ν•˜μ—¬ λ©”μ‹œμ§€κ°€ 올 λ•ŒκΉŒμ§€ λŒ€κΈ°ν•˜λ‹€κ°€ 락이 ν•΄μ œλ˜μ—ˆλ‹€λŠ” 메세지가 였면 λŒ€κΈ°λ₯Ό ν’€κ³  λ‹€μ‹œ 락 νšλ“μ„ μ‹œλ„ν•©λ‹ˆλ‹€. 락 νšλ“μ— μ‹€νŒ¨ν•˜λ©΄ λ‹€μ‹œ 락 ν•΄μ œ λ©”μ‹œμ§€λ₯Ό κΈ°λ‹€λ¦½λ‹ˆλ‹€. 이 ν”„λ‘œμ„ΈμŠ€λ₯Ό νƒ€μž„μ•„μ›ƒ μ‹œκΉŒμ§€ λ°˜λ³΅ν•©λ‹ˆλ‹€.
  3. νƒ€μž„μ•„μ›ƒμ΄ μ§€λ‚˜λ©΄ μ΅œμ’…μ μœΌλ‘œ falseλ₯Ό λ°˜ν™˜ν•˜κ³  락 νšλ“μ— μ‹€νŒ¨ν–ˆμŒμ„ μ•Œλ¦½λ‹ˆλ‹€. λŒ€κΈ°κ°€ 풀릴 λ•Œ νƒ€μž„μ•„μ›ƒ μ—¬λΆ€λ₯Ό μ²΄ν¬ν•˜λ―€λ‘œ νƒ€μž„μ•„μ›ƒμ΄ λ°œμƒν•˜λŠ” μˆœκ°„μ€ νŒŒλΌλ―Έν„°λ‘œ λ„˜κΈ΄ νƒ€μž„μ•„μ›ƒμ‹œκ°„κ³Ό μ•½κ°„μ˜ 차이가 μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.

 

정리

Lettuce

  • κ΅¬ν˜„μ΄ κ°„λ‹¨ν•˜λ‹€
  • Spring data redisλ₯Ό μ΄μš©ν•˜λ©΄ lettuceκ°€ 기본이기 λ•Œλ¬Έμ— λ³„λ„μ˜ 라이브러리λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.
  • Spin Lock 방식이기 λ•Œλ¬Έμ— λ™μ‹œμ— λ§Žμ€ μŠ€λ ˆλ“œκ°€ lock νšλ“ λŒ€κΈ° μƒνƒœλΌλ©΄ redis에 λΆ€ν•˜κ°€ 갈 수 μžˆλ‹€.

Redisson

  • 락 νšλ“ μž¬μ‹œλ„λ₯Ό 기본으둜 μ œκ³΅ν•œλ‹€.
  • pub-sub λ°©μ‹μœΌλ‘œ κ΅¬ν˜„μ΄ λ˜μ–΄μžˆκΈ° λ•Œλ¬Έμ— lettuce와 λΉ„κ΅ν–ˆμ„ λ•Œ redis에 λΆ€ν•˜κ°€ 덜 κ°„λ‹€.
  • λ³„λ„μ˜ 라이브러리λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.
  • lock을 라이브러리 μ°¨μ›μ—μ„œ μ œκ³΅ν•΄μ£ΌκΈ° λ•Œλ¬Έμ— μ‚¬μš©λ²•μ„ 곡뢀해야 ν•œλ‹€.

 

μ°Έκ³ 

  1. https://www.inflearn.com/course/λ™μ‹œμ„±μ΄μŠˆ-μž¬κ³ μ‹œμŠ€ν…œ
  2. https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
  3. https://jypthemiracle.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66
  4. https://velog.io/@minnseong/Spring-synchronized%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C
  5. https://thalals.tistory.com/370