2026-03-20 10:30:07 +08:00
|
|
|
|
"""Small Redis lock helpers for background tasks."""
|
|
|
|
|
|
|
2026-03-30 10:46:35 +08:00
|
|
|
|
import threading
|
2026-03-20 10:30:07 +08:00
|
|
|
|
import uuid
|
2026-03-30 10:46:35 +08:00
|
|
|
|
from dataclasses import dataclass
|
2026-03-20 10:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
import redis
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
2026-03-30 10:46:35 +08:00
|
|
|
|
_redis_lock_client: redis.Redis | None = None
|
|
|
|
|
|
_redis_lock_init_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_redis_lock_client() -> redis.Redis:
|
|
|
|
|
|
"""进程内复用单个 Redis 客户端(decode_responses=False,与锁 token 字节一致)。"""
|
|
|
|
|
|
global _redis_lock_client
|
|
|
|
|
|
if _redis_lock_client is None:
|
|
|
|
|
|
with _redis_lock_init_lock:
|
|
|
|
|
|
if _redis_lock_client is None:
|
|
|
|
|
|
_redis_lock_client = redis.from_url(
|
|
|
|
|
|
settings.redis_url, decode_responses=False
|
|
|
|
|
|
)
|
|
|
|
|
|
return _redis_lock_client
|
|
|
|
|
|
|
2026-03-20 10:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
|
class RedisLockHandle:
|
|
|
|
|
|
key: str
|
|
|
|
|
|
token: bytes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def acquire_redis_lock(key: str, *, ttl_seconds: int) -> RedisLockHandle | None:
|
|
|
|
|
|
"""Acquire a single-owner Redis lock or return None when unavailable."""
|
2026-03-30 10:46:35 +08:00
|
|
|
|
client = _get_redis_lock_client()
|
2026-03-20 10:30:07 +08:00
|
|
|
|
token = uuid.uuid4().hex.encode("utf-8")
|
|
|
|
|
|
if not client.set(key, token, nx=True, ex=ttl_seconds):
|
|
|
|
|
|
return None
|
2026-03-30 10:46:35 +08:00
|
|
|
|
return RedisLockHandle(key=key, token=token)
|
2026-03-20 10:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def release_redis_lock(handle: RedisLockHandle | None) -> None:
|
|
|
|
|
|
"""Release the lock only if we still own it."""
|
|
|
|
|
|
if handle is None:
|
|
|
|
|
|
return
|
2026-03-30 10:46:35 +08:00
|
|
|
|
_get_redis_lock_client().eval(
|
2026-03-20 10:30:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
|
|
|
|
return redis.call("DEL", KEYS[1])
|
|
|
|
|
|
end
|
|
|
|
|
|
return 0
|
|
|
|
|
|
""",
|
|
|
|
|
|
1,
|
|
|
|
|
|
handle.key,
|
|
|
|
|
|
handle.token,
|
|
|
|
|
|
)
|