# π μ°¨κ·Όμ°¨κ·Ό λμμ± μ΄μ ν΄κ²°νκΈ° (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();
}
}
}
μ΄ λ°©μμ κ°μ₯ ν° μ₯μ μ μΆ©λμ΄ λΉλ²νκ² μΌμ΄λλ μν©μμ λ°μ΄ν°μ μ ν©μ±μ μ μ νκ² μ§ν¬ μ μλ€λ κ²μ΄λ€. λ¨μ μΌλ‘λ μ€μ λ°μ΄ν°λ² μ΄μ€μ λ½μ κ±ΈκΈ° λλ¬Έμ λ€μμ μμ²μ μ μ°νκ² λμνμ§ λͺ»νλ€.
# @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
μ΄ λ°μνλ€.
κ·Έλ¦ΌμΌλ‘ νννλ©΄ μλμ κ°λ€.
μ‘°ν λ©μλλ₯Ό @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
# [λ²μΈ] μ‘°ν μμ΄ λ°λ‘ 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 = ?
μ€μ ν μ€νΈλ₯Ό μνν΄λ ν΅κ³Όνλ€.
λ€λ§ 무쑰건 μμ²μ΄ 100λ²λ§ μ¨λ€λ 보μ₯μ΄ μκΈ° λλ¬Έμ μλ λ³΄λ€ λ§μ μμ²μ΄ λ€μ΄μ¨λ€κ³ κ°μ ν λ€ ν μ€νΈλ₯Ό μ§ννλ€.
쿼리μ μλμ λν μ‘°κ±΄μ΄ μκΈ° λλ¬Έμ μ°λ¦¬κ° μꡬμ¬νμ΄ λ§μ‘±νμ§ μλλ€. μ΄λ 쿼리μ μλμ λν 쑰건μ μΆκ°νλ€.
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)