ð ë€ìŽê°ë©°
ì§ííë íë¡ì ížìì ìì ê°ìŽ ìì íì륌 ì¬ë¬ë² íŽëŠí멎 ìŽì ë§ê² íìê° ìŠê°ëë ë¡ì§ì 구ííŽìŒ íë€.
í ëª ì ì¬ì©ìë ì¬ë¬ë² ìì íì륌 ìŠê°ìí¬ ì ìë€.
ìŽì êŽë šíŽ ìŽë»ê² ëìì± ë¬žì 륌 íŽê²°í ì ììì§ì ëí ê³ ë¯Œì ììíë€.
â ëìì± ë¬žì ë?
- ëìì± ë¬žì ê° ë°ìíì§ ìë ìí©
pkê° 1ìž ìì ë°ìŽí°ì ìì íìê° 90ìž ìíìì ì¬ì©ì A ê° 3ë² ìì íì륌 ìŠê°ìí€ê³ ì¬ì©ì B ê° 10ë² ìì íì륌 ìŠê°ì쌰ë€ê³ ê°ì íŽë³Žì.
ì¬ì©ì A | ìì íì | ì¬ì©ì B |
pkê° 1ìž ìì ë°ìŽí°ë¥Œ ì°Ÿëë€ >> ì ìì íìë 90ìŽêµ¬ë! | 90í | |
pk ê° 1ìž ìì ë°ìŽí°ì íì륌 3í ìŠê°ìíšë€. | 93í | |
93í | pkê° 1ìž ìì ë°ìŽí°ë¥Œ ì°Ÿëë€ >> ì ìì íìë 93ìŽêµ¬ë! | |
103í | pk ê° 1ìž ìì ë°ìŽí°ì íì륌 10í ìŠê°ìíšë€. |
ìì²ëŒ ì ìì ìŒë¡ ìì²ìŽ ë°ìëìŽ ê°ê° 3í, 10í륌 ëí 103íê° ê²°ê³Œë¡ ëì¬ ê²ìŒë¡ êž°ëíë€.
- ëìì± ë¬žì ê° ë°ìíë ìí©
ì¬ì©ì A | ìì íì | ì¬ì©ì B |
pkê° 1ìž ìì ë°ìŽí°ë¥Œ ì°Ÿëë€ >> ì ìì íìë 90ìŽêµ¬ë! | 90í | |
90í | pkê° 1ìž ìì ë°ìŽí°ë¥Œ ì°Ÿëë€ >> ì ìì íìë 90ìŽêµ¬ë! | |
pk ê° 1ìž ìì ë°ìŽí°ì íì륌 3í ìŠê°ìíšë€. | 93í | |
pk ê° 1ìž ìì ë°ìŽí°ì íì륌 10í ìŠê°ìíšë€. | ||
100í |
ê·žë¬ë, ëìì± ë¬žì ê° ê³ ë €ëì§ ìëë€ë©Ž ìì²ëŒ ìŒë¶ íìê° ëëœë ê°ë¥ì±ìŽ ì¡Žì¬íë€.
â ëìì± ë¬žì ì ììž - Race Condition
- Race condition ìŽë?
ìŽë ëê° ìŽìì ì°ë ëê° ê³µì ë°ìŽí°ì ì¡ìžì€ê° ê°ë¥í ëìì ë³ê²œì íê³ ì í멎 ë°ìíë 묞ì ìŽë€.
Race Condition ì ìíŽì ë°ìŽí°ì 결곌ë ê³µì© ë°ìŽí°ì ëí ì ê·Œì ëí ììì ìíŽ ë¬ëŒì§ê² ëë€.
ìì ììë¡ ë€ë¥Žê² ìŽìŒêž°íì멎, ì¬ì©ì A ì ì¬ì©ì B ê° ììëë¡ ì ê·Œíê² ë멎 ì ìì ìŒë¡ ê²°ê³Œê° ë°ìëì§ë§ ëìì ì ê·Œíê² ë멎 ì못ë ê²°ê³Œê° ëì€ë ê²ìŽë€.
â 묞ì ê° ë°ìí ìœë
- request body
// Ʞ졎ì request body
{
gameTeamId:1,
cheerCount:101
}
- Repository ìœë
@Modifying
@Query("UPDATE GameTeam t SET t.cheerCount = :cheerCount WHERE t.id = :gameTeamId")
void updateCheerCount(@Param("gameTeamId") Long gameTeamId, @Param("cheerCount") int cheerCount);
- í ì€íž ìœë
@Test
void ëìì_ìì_ìì²ì_볎ëŒ_겜ì°ìë_ì ìì ìŒë¡_ìì²ìŽ_ë°ìëë€() throws InterruptedException {
// given
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(100);
long gameTeamId = 3L;
// when
for (int i = 0; i < 100; i++) {
final int j = i;
executor.execute(() -> {
try {
gameTeamService.updateCheerCount(2L, new GameTeamCheerRequestDto(gameTeamId, j + 1));
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// then
assertThat(gameTeamFixtureRepository.findById(gameTeamId))
.map(GameTeam::getCheerCount)
.get()
.isEqualTo(101);
}
ëìì± ë¬žì ê° ë°ìíë ìœëë ìì ê°ìë€.
íŽëŒìŽìžíž ìž¡ìì Ʞ졎ì íìì ìŠê°ìí¬ íì륌 ëíŽì ìì²ì 볎ëŽë©Ž, ìŽë¥Œ ë°ìíëë¡ íìë€.
ìŠ, Ʞ졎 íìê° 100íìŽê³ ìŠê°ìí¬ íìê° 1ìŽëŒë©Ž íŽëŒìŽìžíž ìž¡ìì 101ì ìì²ìŒë¡ 볎ëë ê²ìŽë€.
í ì€íž 결곌ë ë€ì곌 ê°ë€.
êž°ëíë íìë 1ìì 100ë² ìŠê°ìíš 101 íìë€.
ê·žë¬ë, ì€ì ë°ìë ë°ìŽí°ë 81íìì íìží ì ììë€.
ð ìŽë»ê² íŽê²°íìê¹
- request body
// Ʞ졎ì request body
{
gameTeamId:1,
cheerCount:101
}
// ìì ë request body
{
gameTeamId:1,
cheerCount:1
}
íŽëŒìŽìžíž ìž¡ìì ì§ì Ʞ졎ì íìì ìë¡ ìŠê°í íì륌 ëíŽì ìì²ì 볎ëë ê²ìì, ìŠê°í íìë§ì ìì²ìŒë¡ 볎ëŽëë¡ ìì íë€.
Ʞ졎ì request body ìë Ʞ졎ì íìê° 100íìŽê³ ìŠê°ìí¬ íìê° 1ìŽëŒë©Ž 101ì 볎ëì§ë§, ìì ë ë°©ìììì request body ìë 1ë§ì 볎ëŽë ê²ìŽë€.
@Modifying
@Query("UPDATE GameTeam t SET t.cheerCount = t.cheerCount + :cheerCount WHERE t.id = :gameTeamId")
void updateCheerCount(@Param("gameTeamId") Long gameTeamId, @Param("cheerCount") int cheerCount);
ìì 쿌늬륌 볎멎 ìê² ì§ë§, ìì²ìŽ ë€ìŽìì ë ê·ž ìì ììì ììíìì ìë¡ìŽ cheerCount 륌 ëíë€.
ìŽë ê² ìì íë, ìžì ì°ë ëê° ë°ìŽí°ë¥Œ ìœìëì§ìë êŽê³ê° ììŽì§ë¯ë¡ ìŠ, race condition 묞ì ê° ìì°ì€ë íŽê²°ëìŽ ëìì± ë¬žì 륌 íŽê²°í ì ììë€.
ð ìì¬ì ë ì
ì¬ì€ ìì ë°©ìì, íšê» íë¡ì ížì ì°žì¬íê³ ìë ëë£ë¶ê» ì¶ì²ì ë°ì ë°ìíë ë°©ë²ìŽìë€.
ë°ëŒì ëìì± ë¬žì ì ëí íŽê²°ë°©ììŽ ìŽë€ ê²ë€ìŽ ë ììì§ì ëíŽ ê³µë¶íê³ , íìµíì¬ ì¶íì ìŽë»ê² ê°ì íŽë³Žë©Ž ì¢ìì§ì ëíŽ ê³ ë¯ŒíŽë³Žê³ ì¶ìë€.
ð ëìì± ë¬žì íŽê²° ë°©ì
ë€ì ê°ì륌 ì°žê³ íŽì ìì±íìµëë€ :)
ì¬ê³ ìì€í ìŒë¡ ìì볎ë ëìì±ìŽì íŽê²°ë°©ë² ê°ì - ìžíë°
ëìì± ìŽìë 묎ììžì§ ììë³Žê³ ì²ëЬíë ë°©ë²ë€ì íìµí©ëë€., ëìì± ìŽì ì²ëЬë ìì ìê²! ê°ëší ì¬ê³ ìì€í ìŒë¡ 찚귌찚귌 ë°°ì볎ìžì. ë°±ìë ê°ë°ìëŒë©Ž êŒ ìììŒ í ëìì± ìŽì
www.inflearn.com
ð¥ Synchronized
âïž synchronizedë?
ì¬ë¬ ìì²ìŽ ë€ìŽìì ë 묞ì ê° ë°ìíë ìŽì ë ì€ë ë ê°ì ëêž°íê° ëì§ ìêž° ë묞ìŽë€.
ë°ëŒì synchronized ë thread-safe 륌 ìíŽì ì¬ë¬ê°ì ì€ë ëê° íê°ì ììì ì¬ì©íê³ ì í ë, íì¬ ë°ìŽí°ë¥Œ ì¬ì©íê³ ìë ì€ë ë륌 ì ìžíê³ ë ë°ìŽí°ì ì ê·Œí ì ìëë¡ ë§ë ê²ìŽë€.
ìŽë ë³ìì íšìì ì¬ì© ê°ë¥íë€.
âïž ì¬ì© ìì
@Transactional
public synchronized void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
âïž ëšì
1. @Transactional ìŽë ží ìŽì ì íë¡ì ê°ì²Ž
synchronized ë ì€íë§ë¶ížì @Transactional ìŽë ží ìŽì 곌 íšê» ì¬ì©í멎 ì¬ì€ì íšë ¥ìŽ ìë€.
ìëí멎, @Transactional ìŽë ží ìŽì ì AOP ìŽêž° ë묞ì íë¡ì ê°ì²Žê° ë§ë€ìŽì§ë©° ëìíëë° ìŽ ë íë¡ì ê°ì²Žë ë©ìëì ìê·žëì²ë€ë§ ë³µì¬íŽì€êž° ë묞ì synchronized ë ëëœëë€.
2. íëì íë¡ìžì€ ìììë§ ë³Žì¥ëë€.
í ëì ìë² ëŽë¶ììë synchornized í€ìëê° ìë¯žê° ìë€.
ê·žë¬ë, ìë²ê° ëë ìŽììž ê²œì°ìë íë¡ìžì€ ëŽë¶ììì ì ê·Œë§ì ë§ë synchronized ê° ìë¯žê° ììŽì§ê² ëë€.
ë°ëŒì ìŽë ì€ë¬Žìì ì£Œë¡ ì¬ì©ëì§ ìëë€.
ð¥ MySQL ì Pessimistic Lock (ë¹êŽì ëœ)
âïž Pessimistic Lock ìŽë?
ìŽë ë°ìŽí°ì ëœì 걞ìŽì ì í©ì±ì ë§ì¶ë ë°©ë²ìŽë€.
ìŽë ížëìì ìì ìì ëœì ê±žê³ ììíë€.
ë€ë¥ž ížëìì ììë ëœìŽ íŽì ëêž° ìŽì ì ë°ìŽí°ë¥Œ ê°ì žê° ì ìë€.
ìŽì ê°ìŽ, ížëìì 1ìì ë°ìŽí°ë² ìŽì€ë¡ ì ê·Œíê² ë멎 ë€ë¥ž ížëìì ì ëêž°íŽìŒ íë€.
ìŽë ê² ë멎 ììì ì€ëª í Race Condition ì íŽê²°í ì ìë€. ëœì ìíŽ ëìì ë°ìŽí°ê° ê³µì ëì§ ëª»íêž° ë묞ìŽë€.
âïž ì¬ì© ìì
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
âïž ì£Œìí ì
ë°ëëœì 죌ìíŽìŒ íë€.
ð¥ MySQL ì Optimistic Lock (ëêŽì ëœ)
âïž Optimistic Lock ìŽë?
ì€ì ë¡ ëœì ê±°ë ê²ìŽ ìëëŒ, ë²ì ëì ìŽì©íŽ íŽê²°íë€.
ížëìì 1ìŽ ì»€ë°í ëì 쿌늬ë ë€ì곌 ê°ì ê²ìŽë€.
update set version = version+1, quantity = quantity-10
from stock
where version = 1 and id = 1
ížëìì 2ê° ì»€ë°í ëì 쿌늬ë ë€ì곌 ê°ì ê²ìŽë€.
update set version = version+1, quantity = quantity-5
from stock
where version = 1 and id = 1
ê·žë¬ë, ìì ížëìì 1ì 컀ë°ìŒë¡ ìžíŽ version ì 1ìì 2ë¡ ìŠê°íë€.
ë°ëŒì ížëìì 2ê° ì ë°ìŽížíê³ ì íë ë²ì ìŽ 1ìž ëìì pk ê° 1ìž ë°ìŽí°ë ë ìŽì 졎ì¬íì§ ìëë€.
ìŽ ê²œì°ì ì¡°í륌 ë€ì íê³ , ê·ž ìŽíì ì ë°ìŽížë¥Œ íê² ëë€.
âïž ì¬ì© ìì
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
// ì€ëµ ..
}
@Lock(LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
@Transactional
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findByIdWithPessimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
@Component
public class OptimisticLockStockFacade {
public void decrease(Long id, Long quantity) throws InterruptedException {
// ì
ë°ìŽíž ì€íš ìì ì¬ìë
while (true) {
try {
optimisticLockStockService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
}
ëêŽì ëœì ì¬ì©íë 겜ì°ìë ížëìì 2ì 겜ì°ì²ëŒ, ì¡°ííë ë¹ìì version 곌 ì ë°ìŽížíê³ ì í ëì version ìŽ ìŒì¹íì§ ììŒë©Ž ë€ì ì¡°íê° ìŽë€ì§ë ë¡ì§ìŽ íìíë€.
ë°ëŒì OptimisticLockStockFacade ìì ê°ì ë¡ì§ì ìëíê³ , ìŽê° ì±ê³µíêž°ê¹ì§ ë°ë³µì ìŒë¡ ìëíë€.
âïž ì¥ì
1. ížëìì ìŽ ì»€ë°ëêž° ì ê¹ì§ë ëœì ê±žì§ ìëë€.
컀ë°ëë ìì (ì ë°ìŽíž ëë ìì )ìë§ ëœìŽ ê±žëŠ¬ê³ ìŽì ê¹ì§ë ìë¬Žë° ì¡°ì¹ë¥Œ ì·šíì§ ìë ê²ê³Œ ê°ë€.
2. ë¹êŽì ëœì ë¹íŽ ì±ë¥ìì ìŽì ì ì§ëë€.
ë¹êŽì ëœì ëšìí ì¡°ííê³ ì íë ížëìì ì ì ê·Œë§ì ë§ì§ë§, ëêŽì ëœì ì»€ë° ìŽì ê¹ì§ë ëœì ê±žì§ ìêž°ì ëšì ì¡°ííê³ ì íë ížëìì ì ì ê·Œì íì©íë€.
âïž ëšì
1. ì€íš ì ì¬ìëì ììëë ìê°ìŽ ì¡Žì¬íë€.
2. ì€íš ì ì¬ìë ë¡ì§ì ì§ì ìì±íŽì€ìŒ íë€. (OptimisticLockStockFacadeì decrease)
ð¥ MySQL ì Named Lock
âïž Named Lock ìŽë?
ìŽëŠì ê°ì§ ëœìŽë€.
ì륌 ë€ìŽ, í¹ì ë°ìŽí°ì pk 륌 ëœì ìŽëŠìŒë¡ ì§ì íë€ë©Ž íŽë¹ pk 륌 ê°ì§ ë°ìŽí°ì ì ê·Œíê³ ì íë ížëìì ì íŽë¹ pk 륌 ìŽëŠìŒë¡ íë ëœìŽ ê±žë €ìëì§, ê±žë €ìì§ ììì§ ì¬ë¶ë¥Œ íìžíê³ ì ê·Œí ì ìë ê²ìŽë€.
ì¬êž°ì Lock ì entity ê° ìë ë³ëì ê³µê°ì ì ì¥ëë€. íŽë¹ ìí°í°ê° í¹ì ìì ì ìííë ëììë§ ì¬ì© ê°ë¥í ëœì ë³ëì ìŽëŠìŽë ìë³ìë¡ ì€ì íë€.
âïž ì¬ì© ìì
@Component
public class NamedLockStockFacade {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
public interface LockRepository extends JpaRepository<Stock, Long> {
@Query(value = "select get_lock(:key, 30000)", nativeQuery = true)
void getLock(String key);
@Query(value = "select release_lock(:key, 30000)", nativeQuery = true)
void releaseLock(String key);
}
Named Lock ì ížëìì ì¢ ë£ ìì ìëìŒë¡ ëœìŽ íŽì ëì§ ìêž° ë묞ì ë³ëì ëª ë ¹ìŽë¡ ì§ì íŽì íŽì€ìŒ íë€.
ð¡@Transactional(propagation = Propagation.REQUIRES_NEW)
ìì ìœë륌 ìŽíŽë³Žë©Ž, @Transactional(propagation = Propagation.REQUIRES_NEW) ê° ìë ê²ìŽ ë³Žìžë€.
ìŽë íì decrease ë©ìë륌 ì€í í ëë ìë¡ìŽ ížëìì ìŽ ì€íëëë¡ íë ê²ìŽë€.
ìŽë ììì (ìŽì ì) ížëìì 곌 íŽë¹ ë©ìë륌 ì€íí ëì ížëìì ì ë¶ëЬíë ê²ìŽë€. ìŠ, íëì ížëìì ìŽìë ê²ìŽ ìë¡ ë 늜ì ìŒë¡ ì€íëê² ëë ê²ìŽë€.
ì ë€ë¥ž ëœì 구íí ëë íìíì§ ììë ëœìŽ Named Lock ì 구íí ëë§ íìí ê¹?
- ë¹êŽì ëœ : ë¹êŽì ëœì ë°ìŽí°ë¥Œ ìœêž° ììí ëë¶í° 묎조걎 ëœì ê±°ë ë°©ììŽêž° ë묞ì ížëìì
ì ë¶ëЬíì§ ììë ëë€.
- ìŠ, ëìŒí ë°ìŽí°ì ëìì ì ê·ŒíëëŒë ìŽë¯ž ëœìŽ ê±žë € ìêž° ë묞ì ížëìì ì ë¶ëЬí íìê° ìë ê²ìŽë€.
- ëêŽì ëœ : ëêŽì ëœì ì¬ë¬ ížëìì
ìŽ ëìŒí ë°ìŽí°ë¥Œ ìœê³ ì
ë°ìŽížë¥Œ íëëŒë, ì¶©ëì ë°ìŽí° ë³ê²œ ìì ìë§ ë°ìíë€.
- ëêŽì ëœì ëìì ì¬ë¬ ížëìì ìŽ ê°ì ë°ìŽí°ë¥Œ ìœë ê²ì ë§ì§ ìëë€.
- ì¶©ëì ë°ìŽí° ë³ê²œ ìì ìë§ ë°ìíê² ëë¯ë¡ êµ³ìŽ ížëìì ì ë¶ëЬíì§ ììë ëë€.
- Named Lock : Named Lock ì ì ìì ìŒë¡ ëœì êŽëЬíë€. ë°ëŒì ì¬ë¬ ížëìì ìŽ íëì ëœì ìœê³ ì í멎 묞ì ê° ë°ìí ì ìêž°ì ížëìì ì ë¶ëЬëìŽìŒ íë€.
âïž ì¥ì
- íìììì ìœê² 구íí ì ìë€.
-- 'my_named_lock'ìŽëŒë ìŽëŠì ëœì ì»êž° ìíŽ ìµë 10ìŽê¹ì§ ëêž°
SELECT GET_LOCK('my_named_lock', 10);
ìì ê°ìŽ 10ìŽë¥Œ ì€ì íŽë멎, ëœì 10ìŽê° ì»ì§ 못í멎 íììììŽ ëë êž°ë¥ìŽ MySQL ìë ëŽì¥ëìŽ ìë€.
âïž ëšì
- ížëìì ì¢ ë£ ìì ëœìŽ íŽì ëì§ ìêž° ë묞ì ë³ëì ëª ë ¹ìŽë¡ íŽì íŽì€ìŒ íë€.
- ì€ì ë¡ êµ¬íí ììë ë°ìŽí° ìì€ë¥Œ ë¶ëЬíë ê²ìŽ ì¢ë€.
ð¥ Redis ì Lettuce
âïž Redis ì Lettuce ë°©ììŽë?
ìì ê°ìŽ, ì°ë ë 1ìŽ key ê° 1ìž ë°ìŽí°ì ëíŽ lock ì ì»ê³ ì íë€ë©Ž íì¬ ê±žë € ìë lock ìŽ ìêž°ì ìŽë¥Œ íëíê² ëê³ ì±ê³µìŽ ë°íëë€. ì°ë ë 2 ììë key ê° 1ìž ë°ìŽí°ì ëíŽ lock ì ì»ê³ ì íë€ë©Ž ìŽë¯ž ê±žë € ìë lock ìŒë¡ ìžíŽ ì€íšê° ë°íëë€.
ìŽë, ëœ íëì ì€íší ì°ë ëì 겜ì°ìë spin lock ë°©ììŒë¡ ëêž°íë€. ìŽë ê³ì ëê³ ëë©° ëœì ëêž°íë€ê° íëíë ë°©ììŽë€.
ð¡setnx
redis ì ëª ë ¹ìŽ ì€ íëë¡, "SET if Not eXists" ì ìœììŽë€.
ìŽë 죌ìŽì§ í€ì ëí ê°ì ì€ì íëë°, ë§ìœ íŽë¹ í€ê° ìŽë¯ž 졎ì¬íë€ë©Ž ì묎 ìì ë ìííì§ ìì§ë§ í€ê° 졎ì¬íì§ ìëë€ë©Ž ì§ì ë ê°ìŒë¡ í€ì ê°ì ì€ì íë€.
redisì cli 륌 ìŽì©íŽ íìžíŽë³Žì!
⯠docker exec -it 043d5ca6952f redis-cli
127.0.0.1:6379> setnx 1 value
(integer) 1
127.0.0.1:6379> setnx 1 value
(integer) 0
127.0.0.1:6379> del 1
(integer) 1
127.0.0.1:6379> setnx 1 value
(integer) 1
127.0.0.1:6379>
- setnx 1 value 륌 íë, Ʞ졎ì 1ìŽ ìêž°ì ì±ê³µ (1 ë°í)
- ìŽíì setnx 1 value 륌 íë, Ʞ졎ì 1ìŽ ììŒë¯ë¡ ì€íš (0 ë°í)
- ìŽíì del 1 (key 1ì ìì ) ⇒ ì±ê³µ (1 ë°í)
- ë€ì setnx 1 value 륌 íë, 1 ìŽ ìì ë ìŽíì ìëí ê²ìŽë¯ë¡ ì±ê³µ (1 ë°í)
âïž ì¬ì© ìì
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@Component
public class RedisLockRepository {
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
// key ë stock ì id
// value ë "lock" ìŽëŒë 묞ììŽ
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}
public Boolean unlock(Long key) {
return redisTemplate
.delete(generateKey(key));
}
public String generateKey(Long key) {
return key.toString();
}
}
@Component
public class LettuceLockStockFacade {
public void decrease(Long id, Long quantity) throws InterruptedException {
// ëœìŽ íëí ëê¹ì§ ê³ìíŽì ìë륌 íê³ , ì€íší멎 ì°ë ë륌 100 ë°ëЬìžì»šëì ì ì¬ìŽë€.
while (!repository.lock(id)) {
Thread.sleep(100);
}
try {
stockService.decrease(id, quantity);
} finally {
repository.unlock(id);
}
}
}
ìì ì€ëª íë ê²ê³Œ ê°ìŽ, Lettuce ë°©ìììë spin lock ë°©ììŒë¡ ëœì ì»êž°ë¥Œ ëêž°íêž° ë묞ì ìŽì ëí ë¡ì§ì ê°ë°ìê° ì§ì ìì±íŽì€ìŒ íë€.
while 묞ìì 볌 ì ìë¯, Lock ì íëí멎 StockServiceì decrease ë©ìë륌 ì€ííê³ , íŽë¹ lock ì íŽì íë€.
ê·žë¬ë Lock ì íëíì§ ëª»íë 겜ì°ìë ì°ë ë륌 100 ms ì ë ì€ì§íë€ê°, ê³ìíŽì ìë륌 íë€.
âïž ì¥ì
- 구íìŽ ê°ëšíë€.
- named lock 곌 ê±°ì ì ì¬íì§ë§, ìžì êŽëŠ¬ë¥Œ ì 겜ì°ì§ ììë ëë€.
- Ʞ볞 ëŒìŽëžë¬ëЬìŽêž° ë묞ì ë³ëì ëŒìŽëžë¬ëŠ¬ê° íìíì§ ìë€.
âïž ëšì
- spin lock ë°©ììŽêž°ì ì°ë ëê° ì¬ë¬ê° ëêž° ì€ìŽëŒë©Ž ë ëì€ì ë¶íê° ê° ìë ìë€.
ð¥ Redis ì Redisson
âïž Redis ì Redisson ë°©ììŽë?
Redisson ë°©ìì pub-sub 구조ìŽë€. ë°ëŒì ëêž° ì€ìž ì°ë ëë€ìŽ ê³ìíŽì ëœ íëì ìëíì§ ììë ëë€.
ìëí멎, íëì ìëíë ëœìŽ íŽì ëìì ë ëœ íëì ìëí ì ìëë¡ ë©ìì§ë¥Œ ì ë¬íêž° ë묞ìŽë€.
redis-cli ë¡ ìŽíŽë³Žì.
127.0.0.1:6379> subscribe ch1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch1"
3) (integer) 1
1) "message"
2) "ch1"
3) "hello"
127.0.0.1:6379> publish ch1 hello
(integer) 1
ìì ê°ìŽ, ch1 륌 subscribe íŽëê³ ë€ë¥ž 쪜ìì ch1 ì publish 륌 í멎 subscribe 륌 íŽë 쪜ì ë©ìì§ë¥Œ ì ë¬íë€.
âïž ì¬ì© ìì
implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.25.2'
@Component
public class RedissonLockStockFacade {
public void decrease(Long id, Long quantity) {
RLock lock = redissonClient.getLock(id.toString());
try {
boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("lock íë ì€íš");
return;
}
stockService.decrease(id, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
Redisson ë°©ìììë ëœ íŽì ì íëì ìí ìœë륌 ë°ë¡ 구ííì§ ìê³ , ëŒìŽëžë¬ëЬì ëŽì¥ë ë©ìë륌 ìŽì©í멎 ëë€.
ë°ë³µì ìŒë¡ ëœ íëì ìì²íêž° ìí ìœëë íì ìë€.
âïž ì¥ì
- ëœ íë / ì¬ìë륌 Ʞ볞ìŒë¡ ì ê³µíë€.
- pub / sub ë°©ììŽêž° ë묞ì ë¶íê° ë ê°ë€.
âïž ëšì
- ë³ëì ëŒìŽëžë¬ëŠ¬ë¥Œ ì¶ê°íŽìŒ íë€.
- ëœì ëŒìŽëžë¬ëЬ ì°šììì êŽëЬíêž° ë묞ì ìŽì ëí ë¬ë 컀ëžê° 졎ì¬íë€.
ì¬ìëê° íìíì§ ììŒë©Ž lettuce, íìíì§ ììŒë©Ž redisson ì ì£Œë¡ ìŽì©íë€.
ð íì¬ì ë°©ì곌 ë¹êµíŽë³Žì!
@Modifying
@Query("UPDATE GameTeam t SET t.cheerCount = t.cheerCount + :cheerCount WHERE t.id = :gameTeamId")
void updateCheerCount(@Param("gameTeamId") Long gameTeamId, @Param("cheerCount") int cheerCount);
íì¬ë ìì ê°ìŽ update 쿌늬륌 ì¬ì©íê³ ìë€.
ìŽë, ì ë°ìŽíž í ë íë² ë ìì íì륌 ì¡°ííê³ update 륌 í ëë Ʞ볞ì ìŒë¡ íŽë¹ row ì ëœìŽ ê±žëŠ¬ê² ëêž° ë묞ì ëìì± ìŽìê° ë°ìíì§ ìëë€.
ë€ë¥ž ë°©ë²ë€ê³Œ ë¹êµíŽë³Žì!
1. update 쿌늬 vs ë¹êŽì ëœ
ë¹êŽì ëœì ìŽì©í멎 íŽë¹ row ì 첎ì ëœìŽ ê±žëŠ¬ê² ëë€.
ìŽë¡ ìžíŽ ë€ë¥ž 쪜ìì íŽë¹ row 륌 ëšì ì¡°í ìŽìžì ìì ìŽë, ëœì ê±žê³ ì¶ì 겜ì°ì ëꞰ륌 íŽìŒ íë€.
ë°ëŒì ë¹êµì ì¢ê² update ììë§ ëœìŽ ê±žëŠ¬ë íì¬ì ì¿ŒëŠ¬ê° ë«ë€.
2. update 쿌늬 vs ëêŽì ëœ
ë ë€ update 쿌늬륌 ìŽì©íë¯ë¡ ì¬ì€ì ìì íë ìì ì ëœìŽ ê±žëŠ¬ë ê²ì ê°ë€.
ê·žë¬ë, ëêŽì ëœì 겜ì°ìë ì€íš ìì ì¬ìë ë¡ì§ê³Œ ë²ì ì ëí êŽëŠ¬ê° íìíë€.
ì€íš ìì ì¬ìë ë¡ì§ìŽ íìíë€ë ì ìì ë¹êµì ëœìŽ ì죌 ê±žëŠ¬ì§ ìì ê²ìŒë¡ êž°ëëë 겜ì°ì ëêŽì ëœì ì¬ì©íë€.
ê·žë¬ë, ìì íìì 겜ì°ìë ê²ì ì§í ì€ìë ë ëœìŽ ê±žëŠŽ ê²ìŒë¡ ììíŽìŒ íêž°ì íì¬ì update ì¿ŒëŠ¬ê° ë«ë€.
3. update 쿌늬 vs named lock
ì°ì , named lockì ìŽì©íêž° ìíŽìë ë³ëì 늬ìì€ê° íìíë€.
ëí, ìŽë ëœì ìŽëŠì ìŽì©íŽì í¹ì 조걎ì ë§ê² ìžë°í ëœì ì€ì í ì ìë€.
ê·žë¬ë, íì¬ë ìžë°í ì¡°ì ìŽ íìíì§ë ìì ë¿ëë¬ ëœì ë²ìê° ì»€ì§ ê²œì°ë ìêž°ì named lock ì ëì íë ë§ë í ìŽì ê° ìë€.
4. update 쿌늬 vs ë ëì€
íì¬ ìì íìì 겜ì°ìë ì¹ëª ì ìž ë¹ì§ëì€ ë¡ì§ìŽ ìëë€.
íì ëª ë²ìŽ ëëœëë€ê³ íŽì ì¬ì©ìì 겜íìŽë, ë°ìŽí° ìì ìí¥ì ì£Œì§ ìëë€.
ëšì ìí°í ìž ììì ê°ê¹êž° ë묞ì ìì íìì ëìì± ìŽì륌 ìíŽ ìë¡ìŽ ë°ìŽí°ë² ìŽì€ ì€íì ëì íë ê²ì 묎늬ëŒê³ íëšëë€.
ð¬ ì°žê³ ë§í¬
https://seunghyunson.tistory.com/11
[ACID #2] Atomicityë? ìí 믹í ížëìì ìŽë?
âïž Atomicityë? ð¡ Atomicityë RDBMS륌 ì ìíë ACID ížëìì í¹ì± ì€ Aì íŽë¹íë í¹ì±ì ëë€. íêžë¡ ì§ìí멎 ììì±ìŽëŒë ë»ì ê°ì§ë©° ì ížëìì ìì Atomicityë í¹ì±ìŽ ì€ìíì§ì ëíŽ ì
seunghyunson.tistory.com
https://zzang9ha.tistory.com/443
ì¢ìì êž°ë¥ì íµíŽ ìŽíŽë³Žë ëìì± ìŽì (synchronized)
ìë íìžì, ìŽë² í¬ì€í ììë ëìì±(Concurrency)ì ëíŽ ìŽíŽë³Žê² ìµëë€. (ìì ìœëë ê¹íëžìì íìžíì€ ì ììµëë€.) ëìì±(Concurrency) ê°ë ë€ìŽë² ì¬ì ì ê²ìíŽë³ž ëìì±ì ë€ì곌 ê°
zzang9ha.tistory.com
https://sabarada.tistory.com/175
[database] ëêŽì ëœ(Optimistic Lock)곌 ë¹êŽì ëœ(Pessimistic Lock)
ìë íìžì. ì€ëì ëêŽì ëœê³Œ ë¹êŽì ëœì ê°ë ì ëíŽì ìì볎ë ìê°ì ê°ì žë³Žëë¡ íê² ìµëë€. DB ì¶©ë ìí©ì ê°ì í ì ìë ë°©ë² databaseì ì ê·ŒíŽì ë°ìŽí°ë¥Œ ìì í ë ëìì ìì ìŽ
sabarada.tistory.com