# πŸš— μ°¨κ·Όμ°¨κ·Ό λ™μ‹œμ„± 이슈 ν•΄κ²°ν•˜κΈ° (2)

πŸš— μ°¨κ·Όμ°¨κ·Ό λ™μ‹œμ„± 이슈 ν•΄κ²°ν•˜κΈ° (1) - synchronized (opens new window)
πŸ‘‰ πŸš— μ°¨κ·Όμ°¨κ·Ό λ™μ‹œμ„± 이슈 ν•΄κ²°ν•˜κΈ° (2) - Pessimistic Lock (opens new window)
πŸš— μ°¨κ·Όμ°¨κ·Ό λ™μ‹œμ„± 이슈 ν•΄κ²°ν•˜κΈ° (3) - Optimistic Lock (opens new window)
πŸš— μ°¨κ·Όμ°¨κ·Ό λ™μ‹œμ„± 이슈 ν•΄κ²°ν•˜κΈ° (4) - Named Lock (opens new window)

μž‘μ„±μ— μ‚¬μš©λœ 예제 μ½”λ“œλŠ” concurrency-pessimistic-lock (opens new window)μ—μ„œ 확인해볼 수 μžˆλ‹€.

# 비관적 락

비관적 λ½μ΄λž€, νŠΈλžœμž­μ…˜ 사이에 좩돌이 λ°œμƒν•  κ²ƒμž„μ„ λΉ„κ΄€μ μœΌλ‘œ κ°€μ •ν•œ λ’€ 락을 κ±°λŠ” 방식을 λ§ν•œλ‹€. λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ μ œκ³΅ν•˜λŠ” 락 κΈ°λŠ₯을 ν™œμš©ν•œλ‹€.

νŠΉμ • νŠΈλžœμž­μ…˜μ—μ„œ x-lock을 νšλ“ν•˜κ²Œ 되면 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œλŠ” 락이 ν•΄μ œλ  λ•Œ κΉŒμ§€ λŒ€κΈ°ν•˜κ²Œ λœλ‹€.

# 예제 μ½”λ“œ

예제 μ½”λ“œλŠ” 이전에 synchronizedλ₯Ό ν™œμš©ν•œ λ™μ‹œμ„± 이슈 ν•΄κ²°μ—μ„œ μ‚¬μš©ν•œ Product μ—”ν‹°ν‹°λ₯Ό μž¬μ‚¬μš©ν•  것이닀. μΆ”κ°€λœ 뢀뢄은 x-lock을 건 λ’€ μ‘°νšŒν•˜λŠ” λ©”μ„œλ“œκ°€ μΆ”κ°€ λ˜μ—ˆλ‹€.

MySQL은 κ°€μž₯ μ΅œμ‹  버전인 8.0.30을 ν™œμš© ν–ˆμœΌλ©° κΈ°λ³Έ μŠ€ν† λ¦¬μ§€ 엔진인 InnoDBλ₯Ό μ‚¬μš© ν–ˆλ‹€.

# 비관적 락 적용

비관적 락 μ μš©μ€ @Lock μ• λ…Έν…Œμ΄μ…˜μ„ 톡해 μ‰½κ²Œ 적용이 κ°€λŠ₯ν•˜λ‹€.

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithPessimisticLock(final Long id);
}

@Lock(value = LockModeType.PESSIMISTIC_WRITE)을 μ μš©ν•œ λ’€ μœ„ λ©”μ„œλ“œλ₯Ό μˆ˜ν–‰ν•˜λ©΄ μ•„λž˜μ™€ 같은 쿼리λ₯Ό 확인할 수 μžˆλ‹€.

select product0_.id       as id1_0_,
       product0_.name     as name2_0_,
       product0_.quantity as quantity3_0_
from product product0_
where product0_.id = ? for update

for updateλž€ ν‚€μ›Œλ“œκ°€ μΆ”κ°€λœ 것을 확인할 수 μžˆλ‹€. 이것이 μ˜λ―Έν•˜λŠ” λ°”λŠ” μ‘°νšŒν•˜λŠ” 데이터λ₯Ό μˆ˜μ •ν•˜κΈ° μœ„ν•΄ x-lock을 κ±Έμ—ˆλ‹€λŠ” μ˜λ―Έμ΄λ‹€. λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œλŠ” ν•΄μ œλ  λ•Œ κΉŒμ§€ κΈ°λ‹€λ¦¬κ²Œ λœλ‹€.

μ•„λž˜ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μˆ˜ν–‰ν•΄λ³΄λ©΄ μ •μƒμ μœΌλ‘œ ν†΅κ³Όν•˜λŠ” 것을 확인할 수 μžˆλ‹€.


@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class ProductServiceTest {
    // ...
    @Test
    void λ™μ‹œμ—_100개의_μƒν’ˆμ„_κ΅¬λ§€ν•œλ‹€() throws InterruptedException {
        var product = productRepository.save(new Product("μΉ˜ν‚¨", 100L));

        var executorService = Executors.newFixedThreadPool(10);
        var countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> process(product, countDownLatch));
        }

        countDownLatch.await();

        var actual = productRepository.findById(product.getId()).orElseThrow();

        assertThat(actual.getQuantity()).isEqualTo(0L);
    }

    private void process(final Product product, final CountDownLatch countDownLatch) {
        try {
            productService.purchase(product.getId(), 1L);
        } finally {
            countDownLatch.countDown();
        }
    }
}

img.png

이 방식에 κ°€μž₯ 큰 μž₯점은 좩돌이 λΉˆλ²ˆν•˜κ²Œ μΌμ–΄λ‚˜λŠ” μƒν™©μ—μ„œ λ°μ΄ν„°μ˜ 정합성을 μ μ ˆν•˜κ²Œ 지킬 수 μžˆλ‹€λŠ” 것이닀. λ‹¨μ μœΌλ‘œλŠ” μ‹€μ œ λ°μ΄ν„°λ² μ΄μŠ€μ— 락을 κ±ΈκΈ° λ•Œλ¬Έμ— λ‹€μˆ˜μ˜ μš”μ²­μ— μœ μ—°ν•˜κ²Œ λŒ€μ‘ν•˜μ§€ λͺ»ν•œλ‹€.

# @Lock

@Lock μ• λ…Έν…Œμ΄μ…˜μ€ νŠΈλžœμž­μ…˜ λ²”μœ„ λ‚΄μ—μ„œλ§Œ μœ νš¨ν•˜λ‹€. 비관적 락을 κ±ΈκΈ° μœ„ν•œ LockModeType은 3 가지가 μ‘΄μž¬ν•œλ‹€.

  • PESSIMISTIC_READ: s-lock을 건닀.
  • PESSIMISTIC_WRITE: x-lock을 건닀. μ•žμ„œ λ™μ‹œμ„± 이슈 해결을 μœ„ν•΄ μ μš©ν•œ LockModeType이닀.
  • PESSIMISTIC_FORCE_INCREMENT: 버전 μ—…λ°μ΄νŠΈμ™€ ν•¨κ»˜ x-lock을 μˆ˜ν–‰ν•œλ‹€. version 칼럼이 μΆ”κ°€μ μœΌλ‘œ ν•„μš”ν•œ κ²ƒμœΌλ‘œ νŒλ‹¨λ˜λŠ”λ° μžμ„Έν•œ μ‚¬μš© μ˜ˆμ‹œλŠ” 아직 λ– μ˜€λ₯΄μ§€ μ•ŠλŠ”λ‹€.

@Lock(value = LockModeType.PESSIMISTIC_READ), s-lock을 ν™œμš©ν•  경우 μ•„λž˜μ™€ 같은 쿼리λ₯Ό 확인할 수 μžˆλ‹€.

select product0_.id       as id1_0_,
       product0_.name     as name2_0_,
       product0_.quantity as quantity3_0_
from product product0_
where product0_.id = ? for share

for shareλŠ” s-lock으둜, νŠΈλžœμž­μ…˜μ΄ 끝날 λ•Œ κΉŒμ§€ μ‘°νšŒν•œ λ ˆμ½”λ“œκ°€ λ³€κ²½λ˜μ§€ μ•ŠλŠ” 것을 보μž₯ν•œλ‹€. ν•΄λ‹Ή λ ˆμ½”λ“œλ₯Ό μˆ˜μ •ν•˜λ €λŠ” λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ€ 락이 ν•΄μ œλ  λ•Œ κΉŒμ§€ λŒ€κΈ°ν•˜κ²Œ λœλ‹€. λ‹€λ§Œ s-lock 이기 λ•Œλ¬Έμ— μ—¬λŸ¬ νŠΈλžœμž­μ…˜μ—μ„œ μ‘°νšŒλŠ” κ°€λŠ₯ν•˜λ‹€.

μ΄λŸ¬ν•œ νŠΉμ„± λ•Œλ¬Έμ— κ΅μ°©μƒνƒœ(deadlock)에 빠질 κ°€λŠ₯성이 λ†’λ‹€. κ΄€λ ¨ μ‚¬λ‘€λŠ” 15.7.5.1 An InnoDB Deadlock Example (opens new window)에 잘 μ„€λͺ…λ˜μ–΄ μžˆλ‹€.

κ°„λ‹¨ν•œ μ˜ˆμ‹œλ₯Ό 듀어보면 두 νŠΈλžœμž­μ…˜μ΄ λ™μ‹œμ— s-lock을 톡해 λ ˆμ½”λ“œλ₯Ό 쑰회 ν–ˆλ‹€κ³  κ°€μ •ν•œλ‹€. 두 νŠΈλžœμž­μ…˜μ€ 쑰회된 λ ˆμ½”λ“œλ₯Ό UPDATEν•œλ‹€. UPDATEλ‚˜ DELETEλ₯Ό μœ„ν•΄μ„œλŠ” x-lock이 ν•„μš”ν•˜λ‹€. 두 νŠΈλžœμž­μ…˜μ€ μ„œλ‘œ x-lock을 νšλ“ν•˜κΈ° μœ„ν•΄ s-lock이 ν•΄μ œλ  λ•Œ κΉŒμ§€ κΈ°λ‹€λ¦°λ‹€. κ²°κ΅­ 두 νŠΈλžœμž­μ…˜ λͺ¨λ‘ x-lock을 νšλ“ν•˜μ§€ λͺ»ν•œ 채 deadlock이 λ°œμƒν•œλ‹€.

그림으둜 ν‘œν˜„ν•˜λ©΄ μ•„λž˜μ™€ κ°™λ‹€.

img.png

쑰회 λ©”μ„œλ“œλ₯Ό @Lock(value = LockModeType.PESSIMISTIC_READ)으둜 μˆ˜μ •ν•œ λ’€ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜λ©΄ ERROR log와 ν•¨κ»˜ ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•œλ‹€.

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_READ)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithPessimisticLock(final Long id);
}
2022-12-12 15:56:50.399  WARN 96139 --- [ool-1-thread-10] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2022-12-12 15:56:50.402 ERROR 96139 --- [pool-1-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction

img.png

# [λ²ˆμ™Έ] 쑰회 없이 λ°”λ‘œ UPDATE

예제λ₯Ό μž‘μ„±ν•˜λ˜ 쀑 λ³΅μž‘ν•˜μ§€ μ•Šμ€ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직인데 for updateλ₯Ό 톡해 μ‘°νšŒν•˜μ§€ μ•Šκ³  λ°”λ‘œ UPDATEλ₯Ό 진행해도 λ˜μ§€ μ•Šμ„κΉŒλΌλŠ” 의문이 생겼닀. κ°€λ Ή μ•„λž˜μ™€ 같이 말이닀.

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Modifying
    @Query("UPDATE Product p "
            + "SET p.quantity = p.quantity - :quantity "
            + "WHERE p.id = :id")
    void decreaseQuantity(final Long quantity, final Long id);
}

@Service
public class ProductService {
    // ...
    @Transactional
    public void purchase(final Long id, final Long quantity) {
        productRepository.decreaseQuantity(quantity, id);
    }
}

쿼리λ₯Ό μ‚΄νŽ΄λ³΄λ©΄ μ•„λž˜μ™€ κ°™λ‹€.

update product
set quantity = quantity - ?
where id = ?

μ‹€μ œ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•΄λ„ ν†΅κ³Όν•œλ‹€.

img.png

λ‹€λ§Œ 무쑰건 μš”μ²­μ΄ 100번만 μ˜¨λ‹€λŠ” 보μž₯이 μ—†κΈ° λ•Œλ¬Έμ— μˆ˜λŸ‰ 보닀 λ§Žμ€ μš”μ²­μ΄ λ“€μ–΄μ˜¨λ‹€κ³  κ°€μ •ν•œ λ’€ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν–ˆλ‹€.

img.png

쿼리에 μˆ˜λŸ‰μ— λŒ€ν•œ 쑰건이 μ—†κΈ° λ•Œλ¬Έμ— μš°λ¦¬κ°€ μš”κ΅¬μ‚¬ν•­μ΄ λ§Œμ‘±ν•˜μ§€ μ•ŠλŠ”λ‹€. μ΄λ•Œ 쿼리에 μˆ˜λŸ‰μ— λŒ€ν•œ 쑰건을 μΆ”κ°€ν•œλ‹€.

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Modifying
    @Query("UPDATE Product p "
            + "SET p.quantity = p.quantity - :quantity "
            + "WHERE p.id = :id AND p.quantity > 0")
    void decreaseQuantity(final Long quantity, final Long id);
}

μœ„ μΏΌλ¦¬λŠ” ν•œ λ²ˆμ— μš”κ΅¬μ‚¬ν•­ μ²˜λ¦¬κ°€ κ°€λŠ₯ν•˜μ§€λ§Œ λ°μ΄ν„°λ² μ΄μŠ€μ— ꡉμž₯히 쒅속적인 방법이라고 ν•  수 μžˆλ‹€. κΈ°μ‘΄ 방법인 x-lock을 ν†΅ν•œ 쑰회 이후 UPDATEλ₯Ό μˆ˜ν–‰ν•  경우 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ ˆλ²¨μ—μ„œ μ œμ•½ 쑰건을 확인할 수 μžˆλ‹€λŠ” μž₯점이 μžˆλ‹€. 각 상황에 λ§žμΆ°μ„œ μ μ ˆν•œ 방법을 μ„ νƒν•˜λ©΄ 쒋을 것 κ°™λ‹€.

# 정리

μ§€κΈˆκΉŒμ§€ 비관적 락을 ν™œμš©ν•œ λ™μ‹œμ„± 이슈 ν•΄κ²° 방법에 λŒ€ν•΄ μ•Œμ•„λ³΄μ•˜λ‹€. 비관적 락은 곡유 μžμ›μΈ λ°μ΄ν„°λ² μ΄μŠ€μ— 락을 κ±Έμ–΄ ν•˜λ‚˜μ˜ νŠΈλžœμž­μ…˜λ§Œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ μƒν˜Έλ°°μ œλ₯Ό λ‹¬μ„±ν•œλ‹€. 닀쀑 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„μ™€ 단일 λ°μ΄ν„°λ² μ΄μŠ€μ˜ 경우 μ΄λŸ¬ν•œ 방식은 λ™μ‹œμ„± μ΄μŠˆμ— λŒ€ν•œ λΉ λ₯Έ 해결책이 될 수 μžˆλ‹€.

ν•˜μ§€λ§Œ λ°μ΄ν„°λ² μ΄μŠ€μ— 직접 락을 κ±ΈκΈ° λ•Œλ¬Έμ— 락이 κ±Έλ¦° μžμ›μ— μ ‘κ·Όν•˜λŠ” νŠΈλžœμž­μ…˜μ€ λŒ€κΈ°ν•˜κ²Œ λœλ‹€. νŠΈλžœμž­μ…˜μ΄ λŠ˜μ–΄λ‚ μˆ˜λ‘ λŒ€κΈ°ν•˜λŠ” μ‹œκ°„μ„ κΈΈμ–΄μ§ˆ 것이닀. 즉 μ„±λŠ₯ 상에 λ¬Έμ œκ°€ 생길 수 μžˆλ‹€.

λ‹€μŒ μ‹œκ°„μ—λŠ” λ°μ΄ν„°λ² μ΄μŠ€μ— 직접 락을 걸지 μ•Šκ³  versionμ΄λΌλŠ” 좔가적인 μΉΌλŸΌμ„ 톡해 λ™μ‹œμ„± 이슈λ₯Ό ν•΄κ²°ν•˜λŠ” 방법에 λŒ€ν•΄ μ•Œμ•„λ³Ό μ˜ˆμ •μ΄λ‹€.

# References.

15.7.5.1 An InnoDB Deadlock Example (opens new window)
μž¬κ³ μ‹œμŠ€ν…œμœΌλ‘œ μ•Œμ•„λ³΄λŠ” λ™μ‹œμ„±μ΄μŠˆ 해결방법 (opens new window)

#λ™μ‹œμ„± #비관적 락
last updated: 12/14/2022, 5:20:01 PM