Files
chongming-utils-cache/src/chongming_cache/lock.py
T
2026-08-27 09:42:21 +08:00

93 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""基于缓存的分布式排他锁(租约 + 续约)。
利用后端提供的 ``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)