πŸš€AWS ECS Fargateλ₯Ό ν™œμš©ν•œ 할인 쿠폰 λ°œκΈ‰ μ‹œμŠ€ν…œ - 4(Redis)


Redis λΆ„μ‚° 락

redisλŠ” 자체적으둜 락을 μ œκ³΅ν•˜λŠ” κΈ°λŠ₯이 μ•„λ‹ˆλΌ, Redis의 νŠΉμ§•μΈ μ›μžμ„±κ³Ό Key-Value 데이터 λͺ¨λΈμ„ ν™œμš©ν•΄ 락 κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λŠ” 방식이닀.

락을 μ„€μ •(Lock)

  • Redisμ—μ„œ κ°€μž₯ 많이 μ‚¬μš©ν•˜λŠ” 락 κ΅¬ν˜„ λ©”μ»€λ‹ˆμ¦˜μ€ SET NX(Not Exists) μ˜΅μ…˜μ„ ν™œμš©ν•˜λŠ” 방식이닀.
    1
    2
      Boolean acquired = redisTemplate.opsForValue()  
          .setIfAbsent(lockKey, requestId, Duration.ofSeconds(LOCK_TIMEOUT_SECONDS));
    
    • Key 생성 : 고유 식별 κ°’ lockKeyλ₯Ό μ‚¬μš©μžκ°€ λ§Œλ“€μ–΄ 락을 μƒμ„±ν•œλ‹€.
    • Value μ„€μ • : 락을 νšλ“ν•œ μ‚¬μš©μžμ˜ 고유 μ‹λ³„μž(requestId)λ₯Ό μ €μž₯ν•˜μ—¬ λˆ„κ°€ 락을 가진 μƒνƒœμΈμ§€ μΆ”μ ν•œλ‹€.
    • NX μ˜΅μ…˜ : SET NX μ˜΅μ…˜μ„ μ‚¬μš©ν•˜μ—¬, ν•΄λ‹Ή ν‚€κ°€ 없을 λ•Œλ§Œ 락을 μƒμ„±ν•œλ‹€. 이미 μ‘΄μž¬ν•œλ‹€λ©΄ μƒˆλ‘œμš΄ 락을 μƒμ„±ν•˜μ§€ μ•Šμ•„ μΆ©λŒμ„ λ°©μ§€ν•œλ‹€.
    • TTL(만료 μ‹œκ°„) : 락에 만료 μ‹œκ°„μ„ μ„€μ •(EXμ˜΅μ…˜)ν•˜μ—¬, νŠΉμ • μ‹œκ°„ 이후 Redisκ°€ μžλ™μœΌλ‘œ ν•΄λ‹Ή ν‚€λ₯Ό μ œκ±°ν•œλ‹€ -> λ°λ“œλ½ 방지λ₯Ό λͺ©μ μœΌλ‘œ

μ›μžμ  λ™μž‘

  • Redis의 λͺ¨λ“  λͺ…령은 기본적으둜 단일 μŠ€λ ˆλ“œλ‘œ, λͺ…λ Ή 싀행이 μ™„μ „νžˆ 끝날 λ•ŒκΉŒμ§€ λ‹€λ₯Έ μš”μ²­μ΄ μ²˜λ¦¬λ˜μ§€ μ•ŠλŠ”λ‹€.
  • 즉 RedisλŠ” 락을 μƒμ„±ν•˜λŠ” μž‘μ—…(ν‚€κ°€ μ‘΄μž¬ν•˜μ§€ μ•Šμ„ λ•Œ μ„€μ •)을 μ›μžμ μœΌλ‘œ μ²˜λ¦¬ν•˜λ―€λ‘œ λ™μ‹œμ„± 문제λ₯Ό 방지할 수 μžˆλ‹€.

락의 ν•΄μ œ(Unlock)

  • Redis 락을 ν•΄μ œν•˜λ €λ©΄
    1. μžμ‹ μ˜ 락인지 확인: 락 λ³΄μœ ν•œ μ‚¬μš©μžκ°€ λ‹€λ₯Έ μ‚¬μš©μžκ°€ μ•„λ‹Œ μžμ‹ μž„μ„ κ²€μ¦ν•˜κΈ° μœ„ν•΄, GET λͺ…령을 μ‚¬μš©ν•΄ Key의 Valueλ₯Ό ν™•μΈν•œλ‹€.
    2. 락 ν•΄μ œ μ‹œλ„:
      • 이후, μ˜¬λ°”λ₯Έ μ‹λ³„μžμΌ κ²½μš°μ—λ§Œ DEL λͺ…령을 μ‚¬μš©ν•˜μ—¬ 락을 ν•΄μ œν•œλ‹€.
      • 락 ν•΄μ œ 두 단계λ₯Ό λΆ„λ¦¬ν•˜μ—¬ GET -> DEL μ²˜λ¦¬ν•˜λ©΄ μ›μžμ„±μ΄ 깨질 수 μžˆλ‹€
      • 이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ Lua 슀크립트λ₯Ό μ‚¬μš©ν•˜λŠ” 방식을 μ‚¬μš©ν•  수 μžˆλ‹€
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	String lockKey = LOCK_KEY_PREFIX + couponCode;  
	String script =  
	        "if redis.call('get', KEYS[1]) == ARGV[1] then " +  
	                "   return redis.call('del', KEYS[1]) " +  
	                "else " +  
	                "   return 0 " +  
	                "end";  
	  
	try {  
	    redisTemplate.execute(  
	            new DefaultRedisScript<>(script, Long.class),  
	            Collections.singletonList(lockKey),  
	            requestId  
	    );  
	} catch (Exception e) {  
	    log.warn("락 ν•΄μ œ 쀑 μ—λŸ¬ λ°œμƒ. lockKey: {}, requestId: {}", lockKey, requestId, e);  
	}

Redis 락의 전체 흐름

μ •λ¦¬ν•΄μ„œ 전체적인 흐름을 μ•Œμ•„λ³΄μž

  1. 락 생성 μ‹œλ„
    • SET NX λͺ…령을 톡해 락을 μƒμ„±ν•œλ‹€
    • 락이 μ—†μœΌλ©΄ 생성에 성곡 True λ°˜ν™˜
    • 락이 μ‘΄μž¬ν•˜λ©΄ 생성에 μ‹€νŒ¨ False λ°˜ν™˜
  2. TTL μ„€μ •
    • 락이 생성할 λ•Œ 만료 μ‹œκ°„(EXμ˜΅μ…˜)을 ν•¨κ»˜ μ§€μ •ν•œλ‹€
  3. 락 μž‘μ—… 쀑
    • 락을 얻은 μ‚¬μš©μžλŠ” μ•ˆμ „ν•˜κ²Œ μž‘μ—…μ„ μˆ˜ν–‰ν•œλ‹€
    • 락 μž‘μ—… 도쀑 μž₯μ• κ°€ λ°œμƒν•˜λ”λΌλ„ TTL 에 따라 μžλ™μœΌλ‘œ ν•΄μ œλœλ‹€
  4. 락 ν•΄μ œ
    • μž‘μ—…μ΄ μ™„λ£Œλ˜λ©΄ 락을 μ μœ ν•œ μ‚¬μš©μžκ°€ Key의 Valueλ₯Ό ν™•μΈν•˜κ³  μΌμΉ˜ν•˜λ©΄ DEL λͺ…λ ΉμœΌλ‘œ ν•΄μ œ
    • 락이 이미 λ§Œλ£Œλ˜μ—ˆλ‹€λ©΄, λ‹€λ₯Έ μ‚¬μš©μžκ°€ 락을 μƒˆλ‘œ 생성 κ°€λŠ₯

쿠폰 λ°œκΈ‰ ν”„λ‘œμ νŠΈμ—μ„œ 락 νšλ“ κ΅¬ν˜„ 방식

데이터 톡신 μ‹œκ°„μ— CSMA/CDλΌλŠ” 방식을 λ°°μ› λ‹€. κ°„λ‹¨ν•˜κ²Œ μ„€λͺ…ν•˜λ©΄ μ–΄λ–€ 데이터λ₯Ό 보내기 전에 일단 λ“£κ³  아무도 μ•ˆλ³΄λ‚Όλ•Œ 보내어 좩돌 ν™•λ₯ μ„ 쀄이고 좩돌이 났더라도 μ§€μˆ˜μ μœΌλ‘œ backoffλ₯Ό ν•˜μ—¬ 좩돌이 λ‚  수둝 μ§€μˆ˜μ μœΌλ‘œ λŠ˜μ–΄λ‚˜λŠ” λ²”μœ„ μ•ˆμ—μ„œ random numberλ₯Ό κ³¨λΌμ„œ λ˜μ§€λŠ” λ°©μ‹μœΌλ‘œ 좩돌이 생길 λ•Œ λŒ€μ‘ν•˜λŠ” μΌμ’…μ˜ 방법둠 같은 것이닀.

이걸 λ“£λŠ” μˆœκ°„ μ•„! Redis의 뢄산락도 lock을 μ–»λŠ” κ³Όμ •μ—μ„œ 좩돌이 생길 수 있고 이 방법을 μ μš©ν•˜λ©΄ 효율적으둜 이미 κ²€μ¦λœ 방법을 μ‚¬μš©ν•˜μ—¬ μΆ©λŒμ„ ν•΄κ²°ν•  수 μžˆκ² λ‹€ 라고 생각이 λ“€μ–΄ λ°”λ‘œ μ μš©ν•΄ 보기둜 ν–ˆλ‹€.

ν•˜μ§€λ§Œ CSMA/CD의 방식을 λͺ¨λ‘ κ΅¬ν˜„ν•œ 것은 μ•„λ‹ˆλ‹€. μ΄μœ λŠ” ν™˜κ²½μ΄ λ‹€λ₯΄κΈ° λ•Œλ¬ΈμΈλ° CSMA/CDλŠ” 단일 λ„€νŠΈμ›Œν¬ μ±„λ„μƒμ˜ μΆ©λŒμ„ 감지 및 해결을 ν•˜κΈ°μ— 랜덀 μœˆλ„μš°λ₯Ό 점차적으둜 늘렀 좩돌 λΉˆλ„ 자체λ₯Ό μ€„μ΄λŠ”λ° 쀑점이라면 Redis 락의 경우 λΆ„μ‚° ν™˜κ²½μ—μ„œ λ™μ‹œμ„±μ„ μ œμ–΄λ₯Ό μœ„ν•΄ μ„€κ³„λ˜μ—ˆκ³ , μž¬μ‹œλ„ νƒ€μž„μ•„μ›ƒκ³Ό 지터 μΆ•μ†Œλ₯Ό 톡해 락 성곡 κ°€λŠ₯성을 λ†’μ΄λŠ”λ° 쀑점이 μžˆλ‹€.

κ·Έλž˜μ„œ 좩돌 μ™„ν™”, μ§€μˆ˜μ  λ°±μ˜€ν”„, λžœλ€ν™” 이 μ„Έκ°€μ§€μ˜ μ–΄λ–€ μ•„μ΄λ””μ–΄λŠ” λΉŒλ €μ˜€μ§€λ§Œ 이것을 κ΅¬ν˜„ν•˜λŠ”λ°λŠ” 차이가 μžˆλ‹€.

  1. 좩돌 감지 방식
    • Redis 락은 μΆ©λŒμ„ 감지라기 λ³΄λ‹€λŠ” 경합상황이 ν•΄μ†Œλ˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μž¬μ‹œλ„λ₯Ό ν•œλ‹€. 살짝 κ°„μ ‘μ μœΌλ‘œ μΆ©λŒμ„ 감지
  2. μž¬μ‹œλ„ 방식
    • Redis 락은 μž¬μ‹œλ„λ₯Ό ν•˜λ‹€κ°€ 일정 μ‹œκ°„μ•ˆμ— 락을 얻지 λͺ»ν•˜λ©΄ μ‹œλ„λ₯Ό μ€‘λ‹¨ν•˜κ³  락 생성 μ‹€νŒ¨λ₯Ό λ°˜ν™˜
    • μ΄μœ λŠ” μ΅œλŒ€ν•œ λΉ λ₯Έ 락 νšλ“ λ˜λŠ” μ‹œκ°„ 초과 μ˜ˆμ™Έ μ²˜λ¦¬κ°€ μ£Όμš”ν•œ μš”κ΅¬μ‚¬ν•­μ΄κΈ° λ•Œλ¬Έμ΄λ‹€.
  3. μ§€ν„°μ˜ μ—­ν• 

    지터 : μ§€μˆ˜ λ°±μ˜€ν”„μ—μ„œ λŒ€κΈ° μ‹œκ°„μ„ μ‘°μ •ν•˜κΈ° μš°ν•΄ μ μš©ν•˜λŠ” λžœλ€κ°’

    • CSMA/CDλŠ” 좩돌이 λ§Žμ„ 수둝 랜덀 간격 λ‚΄μ—μ„œ μ„ νƒν•˜λŠ” λ²”μœ„κ°€ 컀진닀
    • ν•˜μ§€λ§Œ Redis락의 경우 λ°˜λŒ€λ‘œ μž¬μ‹œλ„ νšμˆ˜κ°€ λ§Žμ•„μ§ˆμˆ˜λ‘ 지터 λ²”μœ„λ₯Ό 점차 쀄인닀
    • μ΄μœ λŠ” μ΄ˆκΈ°μ—λŠ” 경합상황이 비ꡐ적 λ§Žμ§€λ§Œ μ‹œκ°„μ΄ 지남에 따라 λ‹€λ₯Έ ν΄λΌμ΄μ–ΈνŠΈλ“€μ΄ μžμ—°μŠ€λŸ½κ²Œ μ’…λ£Œ(락 ν•΄μ œ, TTL 만료) 되기 λ•Œλ¬Έμ΄λ‹€
    • μ§€ν„°μ˜ μΆ•μ†Œλ‘œ 였히렀 락 νšλ“ 성곡λ₯  자체λ₯Ό λ†’μ΄κ²Œ λœλ‹€
  4. 락 ν‚€ 생성
    • μΏ ν°μ½”λ“œλ₯Ό 기반으둜 μœ λ‹ˆν¬ν•œ ν‚€λ₯Ό μƒμ„±ν•œλ‹€.
    • ex) coupon:lock:COUPON123
  5. νƒ€μž„μ•„μ›ƒ(νšλ“ μ œν•œ μ‹œκ°„) 및 λ°±μ˜€ν”„ μ΄ˆκΈ°ν™” μ„€μ •
    • backoffTime : μž¬μ‹œλ„ κ°„ λŒ€κΈ° μ‹œκ°„ μ΄ˆκΈ°κ°’
    • deadline : 락 νšλ“ μ‹œλ„λ₯Ό μ–Έμ œκΉŒμ§€ 반볡 할지에 λŒ€ν•œ μ„€μ • 2024-12-13-202229.png
  6. 락 νšλ“ μ‹œλ„
    • isLockHeld()λ₯Ό 톡해 ν˜„μž¬ 락 ν‚€κ°€ 이미 μ‘΄μž¬ν•˜λŠ”μ§€ 확인
    • keyκ°€ μ—†λ‹€λ©΄ NX와 EX μ˜΅μ…˜μ„ μ‚¬μš©ν•˜μ—¬ 락 νšλ“ μ‹œλ„
    • 락을 νšλ“ ν–ˆλ‹€λ©΄ true λ°˜ν™˜ 2024-12-13-202130.png
  7. 락 νšλ“ μ‹€νŒ¨ μ‹œ μž¬μ‹œλ„
    1. μž¬μ‹œλ„ μ’…λ£Œ 쑰건
      • deadline이 μ΄ˆκ³Όλ˜μ—ˆλŠ”κ°€?
      • μž¬μ‹œλ„ 횟수retryCountκ°€ μ΅œλŒ€μ— λ„λ‹¬ν–ˆλŠ”κ°€?
    2. μ§€μˆ˜ λ°±μ˜€ν”„ 적용
      • μ‹€νŒ¨ νšŸμˆ˜μ— 따라 λŒ€κΈ°μ‹œκ°„(backoffTime)이 μ μ§„μ μœΌλ‘œ 증가
      • calculateBackoffTime() 에 랜덀 지터(jitter)λ₯Ό μΆ”κ°€ν•΄ 좩돌 방지
    3. λ°±μ˜€ν”„λ§ŒνΌ λŒ€κΈ° ν›„ μž¬μ‹œλ„: 2024-12-13-202812.png
      • μž¬μ‹œλ„ νšŸμˆ˜κ°€ λŠ˜μ–΄λ‚˜λ©΄ λŠ˜μ–΄λ‚  수둝 λŒ€κΈ° μ‹œκ°„ μžμ²΄λŠ” 늘리 돼 λ²”μœ„(지터) μžμ²΄λŠ” 쀄여 효율적으둜 락 νšλ“μ„ κ°€λŠ₯ ν•˜λ„λ‘ κ΅¬ν˜„
μž¬μ‹œλ„ 횟수 승수(λŒ€κΈ° μ‹œκ°„ μ¦κ°€μœ¨) 지터 λ²”μœ„(%)
1회 x1.5 ±15%
2회 x2.0 ±13.6%
3회 x2.5 ±12.5%
4회 x3.0 ±11.5%

2024-12-13-203250.png 후에 ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜λ©΄μ„œ 지터 λ²”μœ„λ₯Ό 쑰정해봐야겠닀.