1. ๊ฐ์
๋ฒ์จ ๋์์ฑ ๊ด๋ จ ํฌ์คํฐ๋ง 4๊ฐ์งธ์ด๋ค...
์งํ ์ค์ธ ํ๋ก์ ํธ์์ MySQL Lock์ ๊ฑธ์ด ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ์๋๋ฐ ๋ค๋ฅธ ๋ฐฉ๋ฒ๋ ์๊ฒ๋์ด ์๊ฐํ๊ณ ์ ํ๋ค.
๋จ์ผ ํ๊ฒฝ์์ DB์ ๋น๊ด์ ๋ฝ ๋ฑ์ ์ด์ฉํ์ฌ ๋์์ฑ์ ์ ์ดํ ์ ์์ง๋ง, ์ฌ๋ฌ ๋์ DB๊ฐ ์กด์ฌํ๋ ๋ถ์ฐ DB ํ๊ฒฝ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
์ด๋ ๋ถ์ฐ ๋ฝ์ ํ์ฉํด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค. Redis๋ ๋ถ์ฐ๋ฝ์ ํด๊ฒฐํ๊ธฐ ์ํด ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฐฉ๋ฒ ์ค ํ๋์ด๋ค.
2. ๋์์ฑ ๋ฌธ์ ๊ฐ ์ผ์ด๋ ๊ฒฝ์ฐ
๋ค์์ ์ฌ๊ณ ์ํฐํฐ๊ฐ ์๊ณ ์ฌ๊ณ ๊ฐ์๋ฅผ ํ๋ ๊ธฐ๋ฅ์ ํ ์คํธํ๋ ์ฝ๋์ด๋ค.
- Entity
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Stock {
@Id
@GeneratedValue
@Column(name = "stock_id")
private Long id;
private Long quantity;
public void decrease(Long quantity) {
if (this.quantity - quantity < 0) {
throw new RuntimeException("error");
}
this.quantity -= quantity;
}
}
- ์๋น์ค ์ฝ๋
@Service
@RequiredArgsConstructor
@Transactional
public class StockService {
private final StockRepository stockRepository;
public void decrease(Long stockId, Long val) {
Stock stock = stockRepository.findById(stockId).get();
stock.decrease(val);
}
}
- ํ ์คํธ ์ฝ๋
@SpringBootTest
public class StockTest {
@Autowired
private StockRepository stockRepository;
@Autowired
private StockService stockService;
@BeforeEach
public void insert() {
Stock stock = new Stock(1L, 100L);
stockRepository.saveAndFlush(stock);
}
@AfterEach
public void delete() {
stockRepository.deleteAll();
}
@Test
public void decrease_test() {
stockService.decrease(1L, 1L);
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - 1 = 99
assertEquals(99, stock.getQuantity());
}
}
์ฒ์ ์๋์ 100๊ฐ์๊ณ 1๊ฐ๋ฅผ ๊ฐ์์์ผ ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํ๋ ๊ฒ์ผ๋ก ์ฌ๊ณ ๊ฐ์ ๊ธฐ๋ฅ์ ํ ์คํธํ๋ค.
๋ฌด๋ฆฌ ์์ด ํต๊ณผํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
ํ์ง๋ง ์ด ์ฝ๋์ ๋ฌธ์ ์ ์ ๋ฌด์์ผ๊น?
์์ฒญ์ด ๋์์ ์ฌ๋ฌ ๊ฐ๊ฐ ๋ค์ด์จ๋ค๋ฉด ์์์น ๋ชปํ๊ฒ ๋์ํ ์ ์๋ค.
- ๋์์ฑ ํ ์คํธ
@Test
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();
// 100 - (100 * 1) = 0
assertEquals(0, stock.getQuantity());
}
๋ฉํฐ ์ค๋ ๋๋ฅผ ์ด์ฉํ๊ธฐ ์ํด ExecutorService๋ฅผ ์ฌ์ฉํ์๋ค. newFixedThreadPool(32)๋ ์ต๋ 32๊ฐ์ ์ค๋ ๋๋ฅผ ๊ฐ์ง ์ค๋ ๋ ํ์ ์์ฑํ๋ ๋ฉ์๋์ด๋ค. ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ฃผ๋ฌธ ์ฒ๋ฆฌ๋ฅผ ๋์์ ์ฒ๋ฆฌํ๋ค.
for ๋ฐ๋ณต๋ฌธ์ผ๋ก 100๊ฐ์ ์์ฒญ์ ๋ณด๋ธ๋ค.
CountDownLatch๋ฅผ ์ฌ์ฉํ์ฌ 100๊ฐ ์์ฒญ์ด ๋๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋๋ก ํ์๋ค.
์์๋๋ ๋ฐ๋ 100๋ฒ ๊ฐ์๋ฅผ ์ํค๋๊น 0์ด ๋์ผํ๋ค.
ํ์ง๋ง ์ค์ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ๋ค.
์์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋ํ๋๋ ์ด์ ๊ฐ ๋ฐ๋ก ๋์์ฑ ๋ฌธ์ ๋๋ฌธ์ด๋ค.
๋ฌธ์ ๋ Stock ์ํฐํฐ์ decrease() ๋ฉ์๋์์ ๋ฐ์ํ๋ค.
ํ์ฌ ๊ตฌํ์์๋ ์ฌ๊ณ ์๋์ ๊ฐ์์ํค๊ธฐ ์ ์ ์๋์ด ์์๊ฐ ๋๋์ง ํ์ธํ๊ณ , ์์๊ฐ ๋๋ฉด ์์ธ๋ฅผ ๋์ง๋ค. ๊ทธ๋ฌ๋ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์ด ๋ฉ์๋๋ฅผ ํธ์ถํ ๊ฒฝ์ฐ, ์ฒซ ๋ฒ์งธ ์ค๋ ๋๊ฐ ์๋์ ํ์ธํ ํ ๊ฐ์ํ๊ธฐ ์ ์ ๋ค๋ฅธ ์ค๋ ๋๊ฐ ์๋์ ๋ณ๊ฒฝํ ์ ์๋ค. ์ด๋ก ์ธํด ์๋ชป๋ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ ๊ฒ์ด๋ค.
3. ์ง์ Update ์ฟผ๋ฆฌ๋ฌธ์ ๋ ๋ ค ํด๊ฒฐ
update ํ๋ ๊ณผ์ ์ ์ ๋ถ์ํด ๋ณด๋ฉด JPA์ ๋ณ๊ฒฝ ๊ฐ์ง๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋์์ฑ ๋ฌธ์ ๊ฐ ์ผ์ด๋๋ ๊ฑธ ์ ์ ์๋ค. ๋ณ๊ฒฝ ๊ฐ์ง์ ๊ฒฝ์ฐ ๋ฐ๋ก๋ฐ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ์์ ํ๋ ๊ฒ์ด ์๋๋ผ ์ปค๋ฐ ํ๋ ์์ ์ flush๊ฐ ์ผ์ด๋ ์ฟผ๋ฆฌ๊ฐ ๋ ์๊ฐ๊ฒ ๋๋ค. ๊ทธ๋์ ํ ํธ๋์ญ์ ์์ select์ update ํ๋ ์ฌ์ด์ ๋๊ตฐ๊ฐ๊ฐ ๋์์ select ํ๊ณ update ํ๊ฒ ๋๋ฉด ๊ฐฑ์ค ๋ถ์ค์ด ์ผ์ด๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ๋ณ๊ฒฝ ๊ฐ์ง๋ฅผ ํฌ๊ธฐํ๊ณ ์ง์ update ๋ฌธ์ ๋ ๋ ค์ฃผ์ด์ ์ ํฉ์ฑ์ ๋ง์ถฐ์ค ์๋ ์๋ค. update ๋ฌธ์ ๊ฒฝ์ฐ x lock์ด ๊ฑธ๋ฆฌ๊ธฐ ๋๋ฌธ์ ํด๋น ๋ถ๋ถ์์ ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ด ์ผ์ด๋ ๋์์ฑ์ด ๋ฐ์ํ์ง ์๋๋ค.
@Modifying
@Query("update Stock s set s.quantity = s.quantity - :val where s.id = :stockId")
int decrease(@Param("stockId") Long stockId, @Param("val") Long val);
4. synchronized๋ฅผ ํ์ฉํ ํด๊ฒฐ
- ์๋น์ค ์ฝ๋
@Service
@RequiredArgsConstructor
@Transactional
public class StockService {
private final StockRepository stockRepository;
public synchronized void decrease(Long stockId, Long val) {
Stock stock = stockRepository.findById(stockId).get();
stock.decrease(val);
}
}
ํ์ง๋ง ์ฌ์ ํ ์คํจํ๊ฒ ๋๋ค. ์ด์ ๋ ๋ญ๊น?
๊ทธ ์ด์ ๋ ์คํ๋ง์ @Transactional ๋์ ๋ฐฉ์ ๋๋ฌธ์ด๋ค.
์คํ๋ง์ @Transactional์ด ๋ถ์ ๊ฒฝ์ฐ ํ๋ก์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ค. ์์ ์ฝ๋์์ @Transactional ์ด๋ ธํ ์ด์ ์ decrease ๋ฉ์๋์ ์ ์ฉ๋์ด ์๊ธฐ ๋๋ฌธ์, ์ค์ ๋ก ๋ฉ์๋๊ฐ ์คํ๋ ๋๋ decrease ๋ฉ์๋๋ฅผ ํธ์ถํ ๊ณณ์์ ํ๋ก์ ๊ฐ์ฒด๋ฅผ ํตํด ์คํ๋๊ฒ ๋๋ค.
์ฆ, ๋ฉ์๋ ์์ฒด์ ๋ํด์๋ ๋๊ธฐํ๋ฅผ ํ ์ ์์ง๋ง ํธ๋์ญ์ ์ปจํ ์คํธ ๋ด์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ํ๊ธฐ ์ ์ ๋ค๋ฅธ ์ฐ๋ ๋๊ฐ ์ ๊ทผํ๋ค๋ฉด ๋ค๋ฅธ ์ฐ๋ ๋๋ decrease ๋ณ๊ฒฝ์ด ๋ฐ์๋๊ธฐ ์ ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๋ค.
5. ๋ฐ์ดํฐ ๋ฒ ์ด์ค Lock์ ํ์ฉํ ํด๊ฒฐ
Lock์ ๋ํด์ ์์ ์ ์์ธํ ๋ค๋ฃฌ ํฌ์คํฐ๊ฐ ์์ด ๋งํฌ ๋จ๊ฒจ๋๊ฒ ๋ค
6. Redis๋ฅผ ํ์ฉํ ํด๊ฒฐ
Redis๋ฅผ ์ฌ์ฉํ๋ฉด MySQL์ Lock์ ๊ฑธ์ด ํด๊ฒฐํ ์ ์๋ ์ํฉ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
1. Lettuce ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด์ฉ
- setnx ๋ช ๋ น์ด๋ฅผ ํ์ฉํ์ฌ ๋ถ์ฐ ๋ฝ ๊ตฌํ (spin lock ๋ฐฉ์)
- retry ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ๊ตฌํํด์ฃผ์ด์ผ ํ๋ค.
SETNX ๋ช ๋ น์ด๋ "SET if Not eXists"์ ์ฝ์๋ก, ํค๊ฐ ์กด์ฌํ์ง ์์ ๋๋ง ํน์ ๊ฐ์ ์ค์ ํ๋ ๋ช ๋ น์ด์ด๋ค.
์ด ๋ฐฉ์์ mysql์ ์ฌ์ฉํ ๋์ NamedLock๊ณผ ๋น์ทํ ๋ฐฉ์์ด๋ผ๊ณ ํ ์ ์๋ค.
๋จผ์ ๋ค์๊ณผ ๊ฐ์ด ๋ ๋์ค ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ๋ง๋ ๋ค.
@Component
@RequiredArgsConstructor
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
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));
}
private String generateKey(Long key) {
return key.toString();
}
}
- Facade ํด๋์ค ์์ฑ
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public void decrease(Long key, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(key)) {
Thread.sleep(10);
}
try {
stockService.decrease(key, quantity);
} finally {
redisLockRepository.unlock(key);
}
}
}
2. Redisson ์ฌ์ฉ
Redisson์ pub-sub ๊ธฐ๋ฐ์ lock ๊ตฌํ์ ์ ๊ณตํ๋ค.
pub-sub ๊ธฐ๋ฐ์ ์ฑ๋์ ํ๋ ๋ง๋ค๊ณ lock์ ์ ์ ์ค์ธ ์ฐ๋ ๋๊ฐ ๋ค์ lock์ ์ ์ ํ๋ ค๋ ์ฐ๋ ๋์๊ฒ ์ ์ ๊ฐ ๋๋ฌ์์ ์๋ ค์ฃผ๋ฉด์ lock์ ์ฃผ๊ณ ๋ฐ๋ ๋ฐฉ์์ด๋ค.
- ์์กด์ฑ ์ถ๊ฐ
dependencies {
// Redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.17.4'
}
- Facade ํด๋์ค ์์ฑ
@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {
private final RedissonClient redissonClient;
private final StockService stockService;
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 ์ ๊ณ์ ๋ฝ ํ๋์ ๊ณ์ ์๋ํ๋๊ฒ ์๋๊ธฐ ๋๋ฌธ์ Redis์ ๋ถํ๋ฅผ ์ค์ผ ์ ์๋ค.
7. ๊ฒฐ๋ก
์ฒ์ ์ด ํฌ์คํฐ๋ฅผ ์์ฑํ ์ด์ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์ Lock์ ๊ฑฐ๋ ๊ฒ ๋ณด๋ค ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ธ Redis๋ฅผ ์ฌ์ฉํด์ Lock์ ์ฒ๋ฆฌํด ์ฑ๋ฅ์ ํฅ์ ์ํค๊ณ ์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
ํ์ง๋ง ๋จ์ผ์๋ฒ, ๋จ์ผDB๋ฅผ ์ฌ์ฉํ ๋ Redis์ ์ฑ๋ฅ์ด ๋ ๋์ค์ง ์์๋ค.
Redis๋ ๋ถ์ฐ ํ๊ฒฝ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ Lock์ ๊ฑธ์ด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ ๋ ์ฃผ๋ก ์ฌ์ฉํ์๊ณ , TTL์ ์ค์ ํด ๋ฐ๋๋ฝ์ ๋ฐฉ์งํ ์ ์์๋ค.
๋๋ ๋จ์ผ ์๋ฒ & ๋จ์ผ DB๋ฅผ ์ฌ์ฉํ๊ณ ์์๊ณ , ์ถํ์ ์๋ฒ๊ฐ ํ์ฅ๋๋ค๋ฉด ๊ทธ๋ ๋ง์ด๊ทธ๋ ์ด์ ํด๋ ํฐ ๋ฌธ์ ์์ ๊ฒ ๊ฐ์ ๊ธฐ์กด์ ์ฌ์ฉํ๋ MySQL Lock์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค.