Concurrency issues
Concurrency issues
์ฌ๊ณ ์์คํ ์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ ๊ฐ์๋ฅผ ๋ฃ๊ณ ์์ฝํ ๋ด์ฉ์ ๋๋ค.
Race Condition
๊ฒฝ์์ํ๋ ๋ ๊ฐ ์ด์์ ์ค๋ ๋๊ฐ ๊ณต์ ๋ฐ์ดํฐ์ ์ก์ธ์คํ ์ ์๊ณ , ๋์์ ๋ณ๊ฒฝ์ ํ๋ ค๊ณ ํ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋๋ค.
@Service
@RequiredArgsConstructor
public class StockService {
private final StockRepository stockRepository;
public synchronized void decrease(Long id, Long quantity) {
final Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
์ฐ๋ฆฌ๋ ์๋ ํ
์คํธ์์ 0์ ๊ฒฐ๊ณผ๋ฅผ ์์ํ์ง๋ง, ๋์์ ๋ค์ด์ค๋ ์์ฒญ๋ค์ด ๊ฐฑ์ ์ ๊ฐ์ ์ฝ๊ณ ์์
ํ๋ฉด์ ์ค์ ๊ฐฑ์ ์ด ๋๋ฝ๋๋ ํ์์ด ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
select
quantity:5
quantity:5
select
update
quantity:4
quantity:4
update
@Test
void decrease_inventory_concurrent_requests() 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(0, stock.getQuantity());
}
Java Synchronized
synchronized ๋ฅผ ๋ฉ์๋ ์ ์ธ๋ถ์ ๋ถ์ฌ์ฃผ๋ฉด ํด๋น ๋ฉ์๋๋ ํ ๊ฐ์ ์ค๋ ๋๋ง ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ฒ ๋ฉ๋๋ค.
@Transactional
public synchronized void decreases(Long id, Long quantity) {
final Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
.
โ ๏ธ ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
Spring Transactional
์ ๋์ ๋ฐฉ์์ ์๋์ ๊ฐ์ด ์ฐ๋ฆฌ๊ฐ ๋ง๋ ํด๋์ค๋ฅผ ๋ํํ ํด๋์ค๋ฅผ ์๋ก ๋ง๋ค์ด์ ์คํํ๊ฒ ๋ฉ๋๋ค.
public class TransactionService {
private StockService stockService;
...
public void decreases(Long id, Long quantity) {
startTransaction();
stockService.decreases(id, quantity);
endTransaction();
}
...
}
์ฐ๋ฆฌ๊ฐ ๋ง๋ ํด๋์ค๋ฅผ ํ๋๋ก ๊ฐ์ง๊ณ ํธ์ถํ๊ฒ ๋๋๋ฐ ํธ๋์ญ์
์ข
๋ฃ ์ ์ ๋ค๋ฅธ ์ค๋ ๋๊ฐ ๊ฐฑ์ ๋ ์ ๊ฐ์ ์ฝ๊ฒ ๋๋ฉด
๊ฒฐ๊ตญ ์ด์ ๊ณผ ๋์ผํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
์ฌ๊ธฐ์ ๋จ์ํ๊ฒ Transactional Annotation ์ ์ ๊ฑฐํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์๋ ์์ต๋๋ค.
.
โ ๏ธ ํ์ง๋ง, Java Synchronized ์ ๋ฌธ์ ๋ ๋ ์กด์ฌํฉ๋๋ค.
Java Synchronized ๋ ํ๋์ ํ๋ก์ธ์ค ์์์๋ง ๋ณด์ฅ์ด ๋ฉ๋๋ค.
๊ทธ๋ฌ๋ค๋ณด๋ ์๋ฒ๊ฐ 1๋์ผ ๊ฒฝ์ฐ์๋ ๊ด์ฐฎ๊ฒ ์ง๋ง, ์๋ฒ๊ฐ 2๋ ์ด์์ผ ๊ฒฝ์ฐ ๊ฒฐ๊ตญ ์ฌ๋ฌ ์ค๋ ๋์์ ๋์์ ๋ฐ์ดํฐ์ ์ ๊ทผ
ํ ์ ์๊ฒ ๋๋ฉด์ ๋ ์ด์ค ์ปจ๋์
์ด ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
10:00
select
quantity:5
quantity:5
select
10:05
update
quantity:4
quantity:4
update
๋๋ถ๋ถ์ ์ด์ ์๋น์ค๋ 2๋ ์ด์์ ์๋ฒ๋ก ์ด์๋๊ธฐ ๋๋ฌธ์ Java Synchronized ๋ ๊ฑฐ์ ์ฌ์ฉ๋์ง ์๋ ๋ฐฉ๋ฒ์ ๋๋ค.
DataBase Lock
MySQL
Pessimistic Lock
๋น๊ด์ ๋ฝ.
์ค์ ๋ก
๋ฐ์ดํฐ์ Lock
์ ๊ฑธ์ด์ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒRow or Table ๋จ์
๋ก LockingExclusive Lock
์ ๊ฑธ๊ฒ ๋๋ฉฐ, ๋ค๋ฅธ ํธ๋์ญ์ ์์๋Lock ํด์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ ์ ์์
๋ฐ๋๋ฝ
(๋ ๊ฐ ์ด์์ ์์ ์ด ์๋ก ์๋๋ฐฉ์ ์์ ์ด ๋๋๊ธฐ ๋ง์ ๊ธฐ๋ค๋ ์ํ) ์ฃผ์ ํ์์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๊ฑฐ๋ ์์๋๋ค๋ฉด ์ถ์ฒํ๋ ๋ฐฉ์
์ฅ์ .
์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ค๋ฉด
Optimistic Lock
๋ณด๋ค ์ฑ๋ฅ์ด ์ข์ ์ ์๋ค.๋ฝ์ ํตํด ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ดํ๋ฏ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด ๋ณด์ฅ๋๋ค.
๋จ์ .
๋ณ๋์ ๋ฝ์ ์ก๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ ๊ฐ์๊ฐ ์์ ์ ์๋ค.
/** Repository **/
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(@Param("id") Long id);
...
/** Service **/
@Transactional
public void decrease(Long id, Long quantity) {
final Stock stock = stockRepository.findByIdWithPessimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
Optimistic Lock
๋๊ด์ ๋ฝ.
์ค์ ๋ก Lock ์ ์ด์ฉํ์ง ์๊ณ
๋ฒ์ ์ ์ด์ฉ
ํจ์ผ๋ก์จ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ๋จผ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ํ update ์ํ ์, ํ์ฌ ๋ด๊ฐ ์ฝ์
๋ฒ์ ์ด ๋ง๋์ง ํ์ธํ๋ฉฐ ์ ๋ฐ์ดํธ
์ํ๋ด๊ฐ ์ฝ์ ๋ฒ์ ์์ ์์ ์ฌํญ์ด ์๊ฒผ์ ๊ฒฝ์ฐ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ค์ ์ฝ์ ํ์ ์์ ์ ์ํ
์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋์ง ์์ ๊ฒฝ์ฐ ์ถ์ฒํ๋ ๋ฐฉ์
์ฅ์ .
๋ณ๋์ ๋ฝ์ ์ก์ง ์์ผ๋ฏ๋ก Pessimistic Lock ๋ณด๋ค ์ฑ๋ฅ์ ์ด์ ์ด ์๋ค.
๋จ์ .
์ ๋ฐ์ดํธ ์คํจ ์ ์ฌ์๋ ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ์ง์ ์์ฑํด์ผ ํ๋ค.
/** Entity **/
@Version
private Long version;
...
/** Repository **/
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(@Param("id") Long id);
...
/** Service **/
@Transactional
public void decrease(Long id, Long quantity) {
final Stock stock = stockRepository.findByIdWithOptimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
...
/** Facade **/
@Component
@RequiredArgsConstructor
public class OptimisticLockStockFacade {
private final OptimisticLockStockService optimisticLockStockService;
public void decrease(Long id, Long quantity) throws InterruptedException {
while (true) { // ์
๋ฐ์ดํธ ์คํจ ์ ์ฌ์๋
try {
optimisticLockStockService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
}
Named Lock
์ด๋ฆ์ ๊ฐ์ง Metadata Locking
์ด๋ฆ์ ๊ฐ์ง Lock
ํ๋ ํ, ํด์ ํ ๋๊น์ง ๋ค๋ฅธ ์ธ์ ์ ์ด Lock ์ ํ๋ํ ์ ์๋๋ก ํจTransaction ์ข ๋ฃ ์
Lock ์ด ์๋์ผ๋ก ํด์ ๋์ง ์๋ ์
์ฃผ์๋ณ๋์ ๋ช ๋ น์ด๋ก ํด์ ๋ฅผ ์ํํด ์ฃผ๊ฑฐ๋ ์ ์ ์๊ฐ์ด ๋๋์ผ ํด์
๋น๊ด์ ๋ฝ๊ณผ ์ ์ฌํ์ง๋ง ๋ก์ฐ๋ ํ ์ด๋ธ์ด ์๋
๋ฉํ๋ฐ์ดํฐ์ ๋ฝํน
์ ํ๋ ๋ฐฉ์์ปค๋ฅ์ ํ ๋ถ์กฑ ํ์์ ๋ง๊ธฐ ์ํด ๋ฐ์ดํฐ ์์ค๋ฅผ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ ๊ฒ์ ๊ถ์ฅ
์ฃผ๋ก ๋ถ์ฐ๋ฝ ๊ตฌํ ์ ์ฌ์ฉ
์ฅ์ .
Timeout์ ์ฝ๊ฒ ๊ตฌํ ๊ฐ๋ฅ
๋จ์ .
ํธ๋์ญ์ ์ข ๋ฃ ์ ๋ฝ ํด์ , ์ธ์ ๊ด๋ฆฌ ํ์
๊ตฌํ ๋ฐฉ๋ฒ์ด ๋ณต์กํด์ง ์ ์์
/** Repository **/
@Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(@Param("key") String key);
@Query(value = "select release_lock(:key)", nativeQuery = true)
void releaseLock(@Param("key") String key);
...
/** Service **/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
final Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
...
/** Facade **/
@Component
@RequiredArgsConstructor
public class NamedLockStockFacade {
private final LockRepository lockRepository;
private final StockService stockService;
@Transactional
public void decrease(Long id, Long quantity) {
try {
lockRepository.getLock(id.toString());
stockService.decrease(id, quantity);
} finally {
lockRepository.releaseLock(id.toString());
}
}
}
Reference.
Redis
MySQL์ Named Lock๊ณผ ์ ์ฌํ ๋ฐฉ์
Lettuce
setnx
(set if not exist) ๋ช ๋ น์ด๋ฅผ ํ์ฉํ์ฌ ๋ถ์ฐ๋ฝ ๊ตฌํSpin Lock
๋ฐฉ์๋ฝ์ ํ๋ํ๋ ค๋ ์ค๋ ๋๊ฐ ๋ฝ์ ์ฌ์ฉํ ์ ์๋์ง ๋ฐ๋ณต์ ์ผ๋ก ํ์ธํ๋ฉด์ ๋ฝ ํ๋์ ์๋ํ๋ ๋ฐฉ์
์ฌ์๋ ๋ก์ง ๊ฐ๋ฐ ํ์
์ฅ์ .
๊ตฌํ์ด ๋จ์
๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ถํ์(spring data redis ์ฌ์ฉ ์ ๊ธฐ๋ณธ์ ์ผ๋ก Lettuce ์ ์ฉ)
๋จ์ .
Spin Lock ๋ฐฉ์์ด๋ฏ๋ก ๋์์ ๋ง์ ์ค๋ ๋๊ฐ ๋ฝ ํ๋ ๋๊ธฐ ์ํ๋ผ๋ฉด ๋ ๋์ค์ ๋ถํ๊ฐ ๊ฐ ์ ์์
๋ฝ ํ๋ ์ฌ์๋ ๊ฐ ๋๊ธฐ ์๊ฐ๋ ํ์
# key ๋ 1 ์ด๊ณ , value ๋ lock ์ธ ๋ฐ์ดํฐ ์์ฑ
> setnx 1 lock
(integer) 1
# ์ด๋ฏธ 1 ์ด๋ผ๋ ํค๊ฐ ์์ผ๋ฏ๋ก ์คํจ
> setnx 1 lock
(integer) 0
# ํค ์ญ์
> del 1
(integer) 1
/* Repository */
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}
public Boolean unlock(Long key) {
return redisTemplate.delete(generateKey(key));
}
...
/* Facade */
public void decrease(Long key, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(key)) {
Thread.sleep(100);
}
try {
stockService.decrease(key, quantity);
} finally {
redisLockRepository.unlock(key);
}
}
Redisson
pub-sub
๊ธฐ๋ฐ์ผ๋ก Lock ๊ตฌํ ์ ๊ณต์ฑ๋์ ๋ง๋ค๊ณ ๋ฝ์ ์ ์ ์ค์ธ ์ค๋ ๋๊ฐ ๋ฝ ํ๋์ ๋๊ธฐ์ค์ธ ์ค๋ ๋์๊ฒ ๋ฝ ํด์ ๋ผ๋ ๋ฉ์์ง๋ฅผ ์ ์กํ๋ฉด, ๋ฉ์์ง๋ฅผ ์ ๋ฌ๋ฐ์ ์ค๋ ๋๊ฐ ๋ฝ ํ๋์ ์๋ํ๋ ๋ฐฉ์
๋ฝ ํ๋์ ์ํด ๋ฐ๋ณต์ ์ผ๋ก ๋ฝ ํ๋์ ์๋ํ๋ Lettuce ๋๋น ๋ถํ๋ฅผ ์ค์ผ ์ ์์
Thread-1 ----------> Channel -------------------> Thread-2
I'm done Try to get a Lock
# ch1 ์ฑ๋ ๊ตฌ๋
(thread-2)
> subscribe ch1
1) "subscribe"
2) "ch1"
3) (integer) 1
# ๋ฉ์์ง ์ ์ก(thread-1)
> publish ch1 hello
(integer) 1
# thread-2 ๋ฉ์์ง ์์
...
1) "message"
2) "ch1"
3) "hello"
์ฅ์ .
pub-sub ๊ธฐ๋ฐ ๊ตฌํ์ผ๋ก lettuce ๋๋น ๋ ๋์ค์ ๋ถํ๊ฐ ๋ ๊ฐ
๋ฝ ํ๋ ์ฌ์๋๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต
๋จ์ .
๋ณต์กํ ๊ตฌํ
๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์
/* Facade */
public void decrease(Long key, Long quantity) {
RLock lock = redissonClient.getLock(key.toString());
try {
boolean available = lock.tryLock(10, 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๋ฅผ ํ์ฉํ๊ณ ,
์ฌ์๋๊ฐ ํ์ํ ๊ฒฝ์ฐ Redisson์ ํ์ฉ
Finish
Java Synchronized
ํ ๊ฐ์ ์ค๋ ๋๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ ํ
@Transactional ์ ์ฉ ๋ถ๊ฐ
2๋ ์ด์์ ์๋ฒ๋ก ์ด์๋ ๊ฒฝ์ฐ ๋ ์ด์ค ์ปจ๋์ ์ ๋ ๋ค์ ๋ฐ์
DataBase Lock
Pessimistic Lock
๋ก์ฐ, ํ ์ด๋ธ ๋จ์๋ก ๋ฝํน
๋ฝ ํด์ ์ ๊น์ง ๋ค๋ฅธ ์ค๋ ๋๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ ์ ์์(๋ฐ๋๋ฝ ์ฃผ์)
์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋ ๊ฒฝ์ฐ ์ถ์ฒ
Optimistic Lock
๋ฒ์ ์ ์ด์ฉ
์ ๋ฐ์ดํธ ์คํจ ์ ์ฌ์๋
์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋์ง ์์ ๊ฒฝ์ฐ ์ถ์ฒ
์ ๋ฐ์ดํฐ ์คํจ ์ ์ฌ์๋ ๋ก์ง ๊ฐ๋ฐ ํ์
Named Lock
Pessimistic Lock ๊ณผ ์ ์ฌํ์ง๋ง ์ด๋ฆ๋ค ๊ฐ์ง ๋ฐ์ดํฐ์ ๋ฝํน
ํธ๋์ญ์ ์ข ๋ฃ ์ ๋ฝ ํด์ , ์ธ์ ์ ์ง์ ๊ด๋ฆฌ ํ์
์ฃผ๋ก ๋ถ์ฐ๋ฝ ๊ตฌํ ์ ์ฌ์ฉ
Redis
Lettuce
Spin Lock ๋ฐฉ์(๋ ๋์ค์ ๋ถํ ๊ฐ๋ฅ์ฑ ์กด์ฌ)
์ฌ์๋ ๋ก์ง ๊ฐ๋ฐ์ด ํ์
์ฌ์๋๊ฐ ํ์ํ์ง ์์ ๊ฒฝ์ฐ ๊ถ์ฅ
Redisson
pub-sub ๊ธฐ๋ฐ
๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํ๊ณ ๊ตฌํ์ด ๋ณต์ก
์ฌ์๋๊ฐ ํ์ํ ๊ฒฝ์ฐ ๊ถ์ฅ
MySql vs. Redis
Mysql์ ์ฌ์ฉ์ค์ด๋ผ๋ฉด ๋ณ๋ ๋น์ฉ์์ด ์ฌ์ฉ ๊ฐ๋ฅ
ํ์ฉ์ค์ธ Redis๊ฐ ์๋ค๋ฉด ๋ณ๋์ ๊ตฌ์ถ ๋น์ฉ๊ณผ ์ธํ๋ผ ๊ด๋ฆฌ ๋น์ฉ์ด ๋ฐ์
Redis ๋ณด๋ค ์ฑ๋ฅ์ด ์ข์ง ์์(์ด๋์ ๋์ ํธ๋ํฝ๊น์ง๋ ๋ฌธ์ ์์ด ํ์ฉ ๊ฐ๋ฅ)
Mysql ๋ณด๋ค ์ฑ๋ฅ์ด ์ข์
Reference
Last updated