μλ°μμ λμμ±μ ν΄κ²°νλ λ€μν λ°©λ²κ³Ό Redisμ λΆμ°λ½
μ΄λ² ν¬μ€ν μ μ¬μ μ§μμΌλ‘ μ΄μ체μ μ λκΈ°ν μ΄λ‘ μ λν΄ μκ³ μμ΄μΌ μμ½κ² μ΄ν΄ν μ μμΌλ―λ‘, ν·κ°λ¦¬μλ λΆλ€μ μλ ν¬μ€ν μ λ¨Όμ μ½κ³ μ΄λ² ν¬μ€ν μ μ½μ΄μ£Όμλ©΄ κ°μ¬νκ² μ΅λλ€.
곡μ μμμ λν΄ λμμ μ¬λ¬ κ°μ νλ‘μΈμ€κ° μ κ·Όνμ¬ μκΈ°λ κ²½μ μν©(race condition)μ μ°λ¦¬λ λμμ± λ¬Έμ λΌκ³ λ νλ©°, λ μμΈνλ λμΌν νλμ λ°μ΄ν°μ λ κ° μ΄μμ μ€λ λ, νΉμ μΈμ μμ κ°λ³ λ°μ΄ν°λ₯Ό λμμ μ μ΄ν λ λνλ λ¬Έμ λ‘, νλμ μΈμ μ΄ λ°μ΄ν°λ₯Ό μμ μ€μΌ λ, λ€λ₯Έ μΈμ μμ μμ μ μ λ°μ΄ν°λ₯Ό μ‘°νν΄ λ‘μ§μ μ²λ¦¬ν¨μΌλ‘μ¨ λ°μ΄ν°μ μ ν©μ±μ΄ κΉ¨μ§λ λ¬Έμ λ₯Ό λ§ν©λλ€
λ°±μλ μ¦ μλ²κ°λ°μμκ² μ΄λ¬ν λμμ± λ¬Έμ λ λ§€μ° μ€μνλ°, μμ ν¬μ€νΈ μλ¬Έμ λ λμμλ―μ΄ λμμ± λ¬Έμ λ₯Ό μ²λ¦¬νμ§ μμΌλ©΄ μλμ κ°μ μν©μ΄ λ°μνκΈ° λλ¬Έμ λλ€.
- 곡μ μμμΈ μ μ λ³μ μκΈ 10λ§ μμ΄ μλ€κ³ κ°μ νλ€.
- νλ‘μΈμ€ P1μ μκΈ 10λ§ μμ νμΈν μν©μμ νλ‘μΈμ€ P2κ° μκΈ 5λ§ μμ μ κΈνμ¬ μ΄ 15λ§ μμ μκΈμ μ μ₯νλ€.
- νμ§λ§ νλ‘μΈμ€ P1μ μ₯μμλ μμ§ μκΈμ΄ 10λ§ μ μ΄κΈ°μ 10λ§ μμ μΆκ°νλλΌλ 15 + 10 = 25λΌλ κ²°κ³Όκ° μλ 10 + 10 = 20μ΄λΌλ μ΄μκΈμ΄ μ μ₯λκ² λλ€.
κ·Έλ λ€λ©΄ μ΄μ μ΄λ»κ² μ΄λ¬ν λμμ± λ¬Έμ λ₯Ό ν΄κ²°ν μ μμμ§ νλ² μ¬κ³ μμ€ν μ΄λΌλ κ°λ¨ν λ‘μ§μ μμ±νμ¬ μ λ¦¬ν΄ λ³΄κ² μ΅λλ€.
ν μ€νΈ νκ²½μ λ€μκ³Ό κ°μ΅λλ€.
- Apple Silicon (M1)
- Java 11, Spring, JPA, Lombok
- JUnit5
- MySQL, Redis
μ½λλ κΉνλΈμμ νμΈ κ°λ₯ν©λλ€.
λͺ©μ°¨λ λ€μκ³Ό κ°μ΅λλ€.
- μ¬κ³ μμ€ν κΈ°μ΄ λ‘μ§
- λμμ±μ κ³ λ €νμ§ μμ λ‘μ§
- Java Synchronized ν€μλλ₯Ό νμ©ν λκΈ°ν λ‘μ§
- DBμμ μ μ΄νλ λ°©λ²
- Pessimistic Lockμ νμ©ν λκΈ°ν λ‘μ§
- Optimistic Lockμ νμ©ν λκΈ°ν λ‘μ§
- Named Lockμ νμ©ν λκΈ°ν λ‘μ§
- Redisλ₯Ό νμ©νλ λ°©λ²
- Redis Lettuceμ νμ©ν λΆμ° λ½ κ΅¬ν
- 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 μλμ리
- new CountDownLatch(10); μ νμμΌλ‘ Latch ν κ°μλ₯Ό μ§μ νλ€.
- countDown()μ νΈμΆνλ©΄ Latchμ μΉ΄μ΄ν°κ° 1κ°μ© κ°μνλ€.
- 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
- Pessimistic Lock(λΉκ΄μ λ½)
- Optimistic Lock(λκ΄μ λ½)
- Named Lock(λ€μλ λ½)
1. Pessimistic Lock
- Pessimistic Lockμ΄λ μ€μ λ‘ λ°μ΄ν°μ Lockμ κ±Έμ΄μ μ ν©μ±μ λ§μΆλ λ°©λ²μ λλ€.
- Exclusive Lock(λ°°νμ μ κΈ)μ κ±Έκ² λλ©΄ λ€λ₯Έ νΈλμμ μμλ Lock μ΄ ν΄μ λκΈ° μ μ λ°μ΄ν°λ₯Ό κ°μ Έκ° μ μκ² λ©λλ€.
- μμ μμ²μ λ°λ₯Έ λμμ±λ¬Έμ κ° λ°μν κ²μ΄λΌκ³ μμνκ³ λ½μ κ±Έμ΄λ²λ¦¬λ λΉκ΄μ λ½ λ°©μμ λλ€.
- νμ§λ§, Dead Lock(κ΅μ°©μν)μ λΉ μ§ μνμ±μ΄ μμΌλ―λ‘ μ μν΄μΌ ν©λλ€.
Dead Lock(κ΅μ°©μν)λ μ΄μ OS μ 리 λ ν¬μ€ν ν μ μμΌλ μ°Έκ³ νμλ©΄ κ°μ¬νκ² μ΅λλ€.
μμ λμλμ μν κ³Όμ μ λ€μκ³Ό κ°μ΅λλ€.
- Transaction_1μμ tableμ Id 2λ²μ μ½μ ( name = Karol )
- Transaction_2μμ tableμ Id 2λ²μ μ½μ ( name = Karol )
- Transaction_2μμ tableμ Id 2λ²μ nameμ Karol2λ‘ λ³κ²½ μμ² ( name = Karol )
- νμ§λ§ Transaction 1μμ μ΄λ―Έ shared Lockμ μ‘κ³ μκΈ° λλ¬Έμ Blocking
- Transaction_1μμ νΈλμμ ν΄μ (commit)
- Blocking λμ΄μμλ Transaction_2μ update μμ² μ μ μ²λ¦¬
μ½λ
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μμ λ€μ μ‘°ν νμ μμ μ μννλ λ‘€λ°± μμ μ μνν΄μΌ ν©λλ€.
μμ λμλμ μν κ³Όμ μ λ€μκ³Ό κ°μ΅λλ€.
- Aκ° tableμ Id 2λ²μ μ½μ ( name = Karol, version = 1 )
- Bκ° tableμ Id 2λ²μ μ½μ ( name = Karol, version = 1 )
- Bκ° tableμ Id 2λ², version 1μΈ rowμ κ° κ°±μ ( name = Karol2, version = 2 ) μ±κ³΅
- Aκ° tableμ Id 2λ², version 1μΈ rowμ κ° κ°±μ ( name = Karol1, version = 2 ) μ€ν¨
- Id 2λ²μ μ΄λ―Έ versionμ΄ 2λ‘ μ λ°μ΄νΈλμκΈ° λλ¬Έμ Aλ ν΄λΉ rowλ₯Ό κ°±μ νμ§ λͺ»ν¨
- μΏΌλ¦¬κ° μ€ν¨νλ©΄ λ€μ μ‘°ννμ¬ λ²μ μ λ§μΆ ν μ λ°μ΄νΈ 쿼리λ₯Ό λ 리λ κ³Όμ μ λ°λ³΅
μ 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μ Stockμ λ½μ κ±Έμ§ μκ³ , λ³λμ 곡κ°μ Lockμ 건λ€.
- 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λ₯Ό μ΄μ©νμ¬ λ λμ€μ κ°μ΄ μ‘΄μ¬νμ§ μμΌλ©΄ μΈν νκ² νκ³ , κ°μ΄ μΈν λμλμ§ μ¬λΆλ₯Ό λ¦¬ν΄ κ°μΌλ‘ λ°μ λ½μ νλνλ λ°μ μ±κ³΅νλμ§ νμΈν©λλ€.
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 λ°©μμ μμ±νμ§ μμλ λ©λλ€.
μ½λ
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 νλ νλ‘μΈμ€μ λλ€.
- λκΈ° μλtryLock μ€νΌλ μ΄μ μ νμ¬ λ½ νλμ μ±κ³΅νλ©΄ trueλ₯Ό λ°νν©λλ€. μ΄λ κ²½ν©μ΄ μμ λ μλ¬΄λ° μ€λ²ν€λ μμ΄ λ½μ νλν μ μλλ‘ ν΄μ€λλ€.
- pubsubμ μ΄μ©νμ¬ λ©μμ§κ° μ¬ λκΉμ§ λκΈ°νλ€κ° λ½μ΄ ν΄μ λμλ€λ λ©μΈμ§κ° μ€λ©΄ λκΈ°λ₯Ό νκ³ λ€μ λ½ νλμ μλν©λλ€. λ½ νλμ μ€ν¨νλ©΄ λ€μ λ½ ν΄μ λ©μμ§λ₯Ό κΈ°λ€λ¦½λλ€. μ΄ νλ‘μΈμ€λ₯Ό νμμμ μκΉμ§ λ°λ³΅ν©λλ€.
- νμμμμ΄ μ§λλ©΄ μ΅μ’ μ μΌλ‘ falseλ₯Ό λ°ννκ³ λ½ νλμ μ€ν¨νμμ μ립λλ€. λκΈ°κ° ν릴 λ νμμμ μ¬λΆλ₯Ό 체ν¬νλ―λ‘ νμμμμ΄ λ°μνλ μκ°μ νλΌλ―Έν°λ‘ λκΈ΄ νμμμμκ°κ³Ό μ½κ°μ μ°¨μ΄κ° μμ μ μμ΅λλ€.
μ 리
Lettuce
- ꡬνμ΄ κ°λ¨νλ€
- Spring data redisλ₯Ό μ΄μ©νλ©΄ lettuceκ° κΈ°λ³Έμ΄κΈ° λλ¬Έμ λ³λμ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©νμ§ μμλ λλ€.
- Spin Lock λ°©μμ΄κΈ° λλ¬Έμ λμμ λ§μ μ€λ λκ° lock νλ λκΈ° μνλΌλ©΄ redisμ λΆνκ° κ° μ μλ€.
Redisson
- λ½ νλ μ¬μλλ₯Ό κΈ°λ³ΈμΌλ‘ μ 곡νλ€.
- pub-sub λ°©μμΌλ‘ ꡬνμ΄ λμ΄μκΈ° λλ¬Έμ lettuceμ λΉκ΅νμ λ redisμ λΆνκ° λ κ°λ€.
- λ³λμ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©ν΄μΌ νλ€.
- lockμ λΌμ΄λΈλ¬λ¦¬ μ°¨μμμ μ 곡ν΄μ£ΌκΈ° λλ¬Έμ μ¬μ©λ²μ 곡λΆν΄μΌ νλ€.
μ°Έκ³
- https://www.inflearn.com/course/λμμ±μ΄μ-μ¬κ³ μμ€ν
- https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
- 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
- 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
- https://thalals.tistory.com/370