본문 바로가기

portfolio

장고와 레디스 루아스크립트로 선착순쿠폰 만들기

1. 장고 스레드로 선착순쿠폰을 만들려고했는데 동시성 문제로 인해 레디스 루아스크립트를 채택하였다 레디스는 싱글스레드라 루아스크립트로 작성하면 atomic하게 처리한다 이를 통해 동시에 발생하는 여러 요청에서도 쿠폰 발급이 정확하게 처리되도록 보장할 수 있다
 
SADD -> coupon_id 중복을 방지한다(coupon_id는 uuid로 하였다) 
SCARD -> 쿠폰 수를 반환해 인자로 받아올 쿠폰 개수를 3개로 제한한다.
 

 

generate_coupon_script = redis_client.register_script("""
local generated_coupons_key = KEYS[1]
local coupon_id = ARGV[1]
local limit = tonumber(ARGV[2])
local ttl = tonumber(ARGV[3])

local current_count = redis.call("SCARD", generated_coupons_key)

if current_count >= limit then
    return -1
end

redis.call("SADD", generated_coupons_key, coupon_id)

if current_count == 0 then
    redis.call("EXPIRE", generated_coupons_key, ttl)
end

return 1
""")

 
 
 
2. csrf 데코레이터 및 헤더 토큰을 만든다 
 

from django.middleware.csrf import get_token
from django.views.decorators.csrf import csrf_protect

def csrf_token_view(request):
    """
    CSRF 토큰을 생성하여 반환하는 API
    """
    csrf_token = get_token(request)
    return JsonResponse({"csrf_token": csrf_token})


@csrf_protect
def generate_coupon(request):
    """
    CSRF 활성화된 쿠폰 생성 API
    """

 
 
 
3. user_id:2이 쿠폰을 발급하면, Redis의 coupon_user_key에는 쿠폰 ID에 매핑된 user_id:2이 저장된다 쿠폰 사용 시, Redis의 get 명령어로 해당 쿠폰의 발급 사용자(original_user_id)를 조회하여 현재 사용자와 비교하여. 현재 사용자와 발급 사용자가 다를 경우, 쿠폰 사용을 방지한다
 

 

쿠폰아이디를 조회하면 유저아이디가 조회된다

# ttl은 30일로 설정하였다.
def generate_coupon_with_lua(user_id, ttl=2592000, limit=3):
    signer = Signer()
    coupon_id = str(uuid.uuid4())  # 원본 UUID
    signed_coupon_id = signer.sign(coupon_id)  # 서명된 쿠폰 ID

    redis_key = f"user:{user_id}:generated_coupons"
    coupon_user_key = f"coupon:{signed_coupon_id}:user"

    result = generate_coupon_script(keys=[redis_key], args=[signed_coupon_id, limit, ttl])

    if result == -1:
        raise ValidationError("쿠폰 생성 한도를 초과했습니다. (최대 3개)")

    # 사용자 ID와 서명된 쿠폰 ID 매핑 저장
    redis_client.set(coupon_user_key, user_id, ex=ttl)

    return signed_coupon_id




# 사용자와 쿠폰 매핑 확인
    original_user_id = redis_client.get(coupon_user_key)
#  redis_client.set(coupon_user_key, user_id, ex=ttl) 쿠폰아이디를 GET 조회하면 사용자 ID가 조회됨
    if original_user_id.decode("utf-8") != user_id:
        raise ValidationError("이 쿠폰은 다른 사용자에게 발급되었습니다.")

 
 
 
 
4. 만약 또다른 사용자가 coupon_id를 조작하려고하면 서명된 고유의 signed_coupon_id를 만들어 방지한다.
 

 

def validate_and_apply_coupon(user_id, signed_coupon_id, order_amount, discount=10000, limit=3, ttl=2592000):
    signer = Signer()

    # 서명 검증
    try:
        coupon_id = signer.unsign(signed_coupon_id)  # 서명 검증
    except Exception:
        raise ValidationError("쿠폰 ID가 조작되었습니다.")