"""基于缓存的分布式排他锁(租约 + 续约)。 利用后端提供的 ``set_nx`` 原子原语实现跨进程/跨主机的排他锁: - ``acquire``(SET NX)保证全局只有一方成功,key 带 TTL 租约,防止持有方宕机导致死锁。 - 持有方通过后台任务定期 ``touch`` 续约;续约前先校验 token,避免误续别人抢到的锁。 - ``release`` 同样先校验 token,仅当仍是自己持有时才删除,杜绝误删他人锁。 适用后端: - ``redis``:真正的跨主机共享,多实例部署的推荐后端。 - ``diskcache``:进程安全(SQLite),可在单机多进程/共享卷场景使用,不可跨无共享文件系统的主机。 """ import asyncio import os import socket import uuid from typing import Any from chongming_cache.cache import Cache class DistributedLock: """租约式分布式排他锁,基于集群共享缓存。 Args: cache: ``chongming_cache.Cache`` 实例(后端可为 redis 或 diskcache)。 key: 锁标识(会被后端前缀命名空间隔离)。 token: 持锁者标识;缺省生成本机唯一 token。 ttl: 租约有效期(秒),期间未续约则锁自动失效。 renew_interval: 续约周期(秒),须小于 ttl。 """ def __init__( self, cache: Cache, key: str, token: Any | None = None, ttl: float = 30.0, renew_interval: float = 10.0, ): self._cache = cache self._key = key self._token = token if token is not None else self._new_token() self._ttl = ttl self._renew_interval = min(renew_interval, ttl / 2) self._renew_task: asyncio.Task | None = None @staticmethod def _new_token() -> str: return f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4().hex}" @property def token(self) -> Any: return self._token def acquire(self) -> bool: """尝试获取锁。成功(全局唯一持有者)返回 True,否则返回 False。""" return bool(self._cache.backend.set_nx(self._key, self._token, expire=self._ttl)) def _holding(self) -> bool: return self._cache.get(self._key, None) == self._token def start(self) -> None: """获取成功后启动后台续约;已启动则忽略。""" if self._renew_task is None: self._renew_task = asyncio.create_task(self._renew_loop()) async def _renew_loop(self): while True: try: await asyncio.sleep(self._renew_interval) except asyncio.CancelledError: return # 仅在自己仍持有(token 匹配)时续约;锁已丢失则停止续约 if not self._holding(): return self._cache.touch(self._key, self._ttl) async def stop(self) -> None: """取消后台续约任务。""" if self._renew_task is not None: self._renew_task.cancel() try: await self._renew_task except (asyncio.CancelledError, Exception): pass self._renew_task = None def release(self) -> None: """受保护释放:仅当仍由本 token 持有时才删除锁。""" if self._holding(): self._cache.delete(self._key)