concurrency_safe()
컨ν
μ€νΈ λ§€λμ μκ°λ μ§: 2025-06-07
λμΌν μ¬μ©μ μμ²μ΄ λμμ μ¬λ¬ λ² μ²λ¦¬λλ©΄ μ€λ³΅ κ²°μ , μ€λ³΅ λ±λ‘, μν κΌ¬μ λ±μ λ¬Έμ κ° μκΈ°κΈ° μ½μ΅λλ€.
μ΄ κΈμμλ μ κ° μ§μ νμν΄μ λ§λ€μλ Django + Redis κΈ°λ°μ κ°λ¨νκ³ μμ νκ² λμμ± μ μ΄λ₯Ό ꡬνν μ μλ concurrency_safe()
컨ν
μ€νΈ λ§€λμ λ₯Ό μκ°ν©λλ€.
concurrency_safe()
λ λ€μκ³Ό κ°μ λͺ©μ μ κ°κ³ μ€κ³λμμ΅λλ€:
import logging
import time
from contextlib import contextmanager
from django.core.cache import cache
logger = logging.getLogger(__name__)
class ConcurrencyError(Exception):
"""λμμ± μΆ©λλ‘ μΈν μλ¬"""
@contextmanager
def concurrency_safe(
cache_key: str,
ttl: int = 3,
wait_timeout: int = 0,
exception_cls=ConcurrencyError,
blocked_message="μ μ ν λ€μ μλν΄μ£ΌμΈμ.",
blocked_log_level=logging.WARNING,
):
"""
Redis lock κΈ°λ° μ¬μ©μλ³ μμ² λμμ± μ μ΄ context manager
Args:
cache_key (str): κ³ μ ν λ½ μλ³ ν€ (ex. `"lock:purchase:{user_id}:{ticket_id}"`)
ttl (int): lock μ μ§ μκ° (μ΄)
wait_timeout (int): lock νλ λκΈ° μκ° (μ΄), κΈ°λ³Έ 0μ΄ β λκΈ° μμ΄ μ¦μ μ€ν¨
exception_cls: μμ²μ΄ μ°¨λ¨ λμμλ λ°μμν¬ μ€λ₯ ν΄λμ€
blocked_message: μμ²μ΄ μ°¨λ¨ λμμλ μ μ μκ² νμν μ€λ₯ λ©μμ§
blocked_log_level: μμ²μ΄ μ°¨λ¨ λμμλ λ¨κΈΈ μ°¨λ¨ λ‘κ·Έμ λ‘κ·Έλ 벨
Usage:
with concurrency_safe("my_key:{user_id}:{ticket_id}"):
# λμμ± μ μ΄κ° νμν μ½λ
"""
start_time = time.time()
while True:
acquired = cache.add(cache_key, "1", timeout=ttl)
if acquired:
try:
yield
finally:
cache.delete(cache_key)
return
if time.time() - start_time > wait_timeout:
logger.log(
blocked_log_level, f"[concurrency_safe] lock νλ μ€ν¨: {cache_key=}"
)
raise exception_cls(blocked_message)
time.sleep(0.1)
# νΉμ μ μ - νΉμ 보μ μ€λ³΅ μ§κΈ λ°©μ§
cache_key_enum = CacheKey.MISSION_USER_GIVE_REWARD_LOCK
cache_key = cache_key_enum.code.format(user_id=user_id, reward_id=reward_id)
with concurrency_safe(cache_key=cache_key, ttl=cache_key_enum.timeout):
check_n_setup_mapping_n_give_reward(
user,
mission,
reward,
user_action_log,
is_manual=is_manual,
is_debug=is_debug,
)
@action(detail=False, methods=["put"], url_path="refresh")
def refresh(self, request, *args, **kwargs):
if Environment.get_member(settings.ENV).is_dev():
task = dodo_generate_buyer_interested_product.delay(self.partner.id)
return TaskResponse(task)
with concurrency_safe( # 10λΆ κ°κ²© μμ² μ μ΄
cache_key=CacheKey.DODO_REFRESH_INTERESTED_PRODUCT.get(
user_id=request.user.id
),
ttl=CacheKey.DODO_REFRESH_INTERESTED_PRODUCT.timeout,
exception_cls=ValidationError,
blocked_message="μ μ ν λ€μ μλν΄μ£ΌμΈμ (λ§μ§λ§ κ°±μ ν, 10λΆ λ€ λΆν° κ°λ₯)",
):
task = dodo_generate_buyer_interested_product.delay(self.partner.id)
return TaskResponse(task)
SETNX
μν μ νλ cache.add()
λ‘ λ½μ μλyield
λΈλ‘ μ€νcache.delete()
λ‘ λ½ ν΄μ λ½μ νλνμ§ λͺ»ν κ²½μ°:
wait_timeout
λ΄ μ¬μλ (0.1μ΄
κ°κ²©)cache
(Redis μ°λ)λ‘ κ΅¬ν β μΈλΆ μμ‘΄μ± μμ΄ μ¬μ© κ°λ₯concurrency_safe()
λ Django + Redis νκ²½μμ λ°μν μ μλ μ€λ³΅ μμ² λ¬Έμ λ₯Ό κ°λ¨νκ² ν΄κ²°ν μ μλ λꡬμ
λλ€.
ν¬λ¦¬ν°μ»¬ν μν λ³ν λ‘μ§μ κ°λ³κ² λΆμ¬ μ€λ³΅ μ€νμ λ°©μ§νκ³ , μ΄μ μ€ λ¬Έμ λ₯Ό μ€μ΄λ λ° ν° λμμ΄ λ©λλ€.