Skip to main content

globallock

统一接口的分布式锁管理器,支持 Redis、Django-Redis、etcd、ZooKeeper 四种后端。

名称 globallock 中有两个 l。


安装

pip install globallock

快速开始

Redis 后端(默认)

from globallock import GlobalLockManager

config = {
    "global_lock_engine_options": {
        "host": "localhost",
        "port": 6379,
        "db": 0,
    },
}
lockman = GlobalLockManager(config)

with lockman.lock("my_lock", timeout=60, blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        # 获得锁,执行临界区代码
        ...
    else:
        # 未获得锁,多数情况下什么都不做
        ...

etcd 后端

from globallock import GlobalLockManager
from globallock import ETCD_GLOBAL_LOCK_CLASS

config = {
    "global_lock_engine_class": ETCD_GLOBAL_LOCK_CLASS,
    "global_lock_engine_options": {
        "host": "localhost",
        "port": 2379,
    },
}
lockman = GlobalLockManager(config)
with lockman.lock("my_lock", timeout=60, blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        ...

ZooKeeper 后端

from globallock import GlobalLockManager
from globallock import ZOOKEEPER_GLOBAL_LOCK_CLASS

config = {
    "global_lock_engine_class": ZOOKEEPER_GLOBAL_LOCK_CLASS,
    "global_lock_engine_options": {
        "hosts": "localhost:2181",
    },
}
lockman = GlobalLockManager(config)
with lockman.lock("my_lock", blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        ...

ZooKeeper 后端不支持 timeout 参数(锁不会自动超时释放)。

Django 集成

# settings.py
GLOBAL_LOCK_CONFIG = {
    "global_lock_engine_options": {
        "redis-cache-name": "default",
    },
}
from globallock.django import get_default_global_lock_manager

lockman = get_default_global_lock_manager()
with lockman.lock("my_lock", timeout=60) as lock:
    if lock.is_locked:
        ...

配置项说明

GlobalLockManager(config)

config 是一个字典,支持以下键:

键 类型 默认值 说明
global_lock_engine_class str globallock.redis_global_lock.RedisGlobalLock 锁定引擎类的完整路径,可使用 REDIS_GLOBAL_LOCK_CLASS、DJANGO_REDIS_GLOBAL_LOCK_CLASS、ETCD_GLOBAL_LOCK_CLASS、ZOOKEEPER_GLOBAL_LOCK_CLASS 常量
global_lock_engine_options dict {} 传递给后端驱动的连接参数
global_lock_key_prefix str _glocks: 锁 key 的前缀,不同后端会自动转换为合适的格式(见"key 前缀策略")
timeout float / None None(Redis 引擎默认 30) Redis 锁的 TTL / etcd lease 时长(秒),到点自动过期回收;非“最小锁定时长”。ZooKeeper 后端不使用该参数(锁无 TTL,None 即可,实际阻塞超时用 blocking_timeout)
sleep float 0.1 轮询等待锁的间隔(秒)
blocking bool True 是否阻塞等待锁
blocking_timeout float / None None 阻塞等待超时(秒),超时未获得锁则返回 False
auto_renew bool True(Redis 引擎) 是否开启看门狗续期(仅 Redis 后端)
renew_interval float 5(Redis 引擎) 看门狗续期间隔(秒)

timeout、sleep、blocking、blocking_timeout 也可以在 lockman.lock() 调用时传入,会覆盖 config 中的默认值。

lockman.lock(name, ...)

参数 类型 默认值 说明
name str 必填 锁的名称,同一时间同一名称的锁只能被一个进程持有
timeout float / None config 中的值 Redis 锁的 TTL / etcd lease 时长(秒);ZooKeeper 后端不使用该参数,实际阻塞超时用 blocking_timeout
sleep float config 中的值 / 0.1 轮询间隔(秒)
blocking bool config 中的值 / True 是否阻塞
blocking_timeout float / None config 中的值 阻塞超时(秒)

锁实例方法 / 属性

方法/属性 类型 说明
lock.acquire() bool 主动请求锁,成功返回 True
lock.release() None 释放锁
lock.is_locked bool 当前是否持有锁
lock.lock_key str 锁在存储引擎中的实际 key

Manager 生命周期管理

GlobalLockManager 会跟踪它创建的所有锁实例。当 Manager 生命周期消亡时, 统一释放所有仍被持有的锁,并停止对应的看门狗续期线程。有三种触发方式:

  1. 显式调用 lockman.close():

    lockman = GlobalLockManager(config)
    lockman.lock("my_lock", ...)
    lockman.close()   # 释放所有仍持有的锁
    
  2. with 语法(退出块时自动 close()):

    with GlobalLockManager(config) as lockman:
        with lockman.lock("my_lock", timeout=5) as lock:
            ...
        # 退出 with 块时已统一释放所有锁
    
  3. 进程退出兜底:GlobalLockManager 构造时通过 atexit 注册 close(), 进程正常退出时自动释放所有仍持有的锁。

close() 是幂等的,只释放仍被持有的锁,已释放的锁会被跳过。

说明:Manager 管理的是自己创建的锁实例。锁本身也遵循 with lock 或 lock.release() 的常规用法,两种方式可并存(前者用于 Manager 生命周期, 后者用于单个锁的即时释放)。


后端引擎说明

依赖安装

各后端的 Python 驱动需要额外安装:

后端 安装命令
RedisGlobalLock pip install redis
DjangoRedisGlobalLock pip install django-redis
EtcdGlobalLock pip install etcd3
ZookeeperGlobalLock pip install kazoo

这些驱动未声明在 globallock 的运行时依赖中,需使用者自行安装。

tenacity 兼容性

etcd3 0.12.0 需要 tenacity<8。如使用 etcd 后端,需安装兼容版本:

pip install 'tenacity<8' etcd3

Key 前缀策略

统一配置 global_lock_key_prefix(默认 _glocks:),各后端自动转换:

后端 实际 key 示例 说明
Redis _glocks:my_lock 直接使用原始前缀,: 是 Redis 的 namespace 分隔符
etcd _glocks/my_lock : 替换为 /,去掉前导 /(etcd3 Lock 内部添加 /locks/ 前缀)
ZooKeeper /_glocks/my_lock : 替换为 /,确保以 / 开头(ZooKeeper 路径要求)

如果锁名称中包含 :,该字符会保留不变(例如 lock_name = "event:test",Redis 中为 _glocks:event:test)。

ZooKeeper 后端注意事项

  • timeout 参数无效(锁不会自动释放)。
  • 进程被 kill -9 杀死后,其他进程约 10 秒后可重新获得锁。
  • 进程正常退出时,ZooKeeper 客户端会自动关闭(通过 atexit 注册清理)。

看门狗续期(Watchdog / Auto-Renew)

Redis 锁的释放依赖 TTL 超时,而不像 etcd / ZooKeeper 那样由连接会话自动回收。 这里的 timeout 是 Redis 锁的 TTL(lease 时长 / 键的过期时间),也就是“锁的键在 Redis 中多久后过期”, 不是“任务至少要锁定的时长”,也不是“任务持有锁的上限时长”。它本质上是一个崩溃回收的安全网: 进程崩溃后,锁在 timeout 后自动过期被回收,避免长期残留。

因此 timeout 的核心权衡是:

  • timeout 设得太小:进程崩溃后锁回收较快(残留时间短),但续期间隔 renew_interval 必须小于 timeout, 否则看门狗可能来不及续期就过期;
  • timeout 设得太大:进程崩溃后锁长时间残留,阻塞其他进程获取,影响可用性。

开启 auto_renew=True(Redis 引擎默认开启)后,成功获得锁时,所属 GlobalLockManager 会启动一条后台守护线程(整个 Manager 只有一条,懒启动于首次 acquire()), 该线程按各锁自身 renew_interval 分时续期:每把锁记录上次续期时间,仅当到期时调用 redis.Lock.reacquire() 重置 TTL,并精确休眠到最早的下次唤醒时刻 (默认每 renew_interval=5 秒一次;不同间隔的锁互不牵累,长间隔锁不会被短间隔锁拖累成高频刷新)。 未显式设置 timeout 时,Redis 引擎默认 timeout=30(续期兜底,保证锁有 TTL 可续期)。 效果:

  • 进程存活期间,锁被持续续期,临界区再长也不会过期;
  • 进程崩溃/被杀时,守护线程随之消亡,锁在 timeout 后自然过期回收。
  • 调用 GlobalLockManager.close() 会停止守护线程并统一释放所有锁。

使用示例

config = {
    "auto_renew": True,          # 开启看门狗续期(Redis 引擎默认 True,可省)
    # "renew_interval": 5,       # 自定义续期间隔(秒),Redis 引擎默认 5
    "global_lock_engine_options": {"host": "localhost", "port": 6379},
}
lockman = GlobalLockManager(config)

# 即使 timeout=5,临界区执行 30 秒锁也不会过期
with lockman.lock("my_lock", timeout=5) as lock:
    if lock.is_locked:
        ...

注意事项

  • auto_renew 仅对 Redis 后端(RedisGlobalLock / DjangoRedisGlobalLock)有效; etcd / ZooKeeper 由连接会话自动回收,无需续期。
  • auto_renew 依赖锁有 TTL;未显式设置 timeout 时,Redis 引擎默认 timeout=30。
  • 开启 auto_renew 时,底层 redis.Lock 会以 thread_local=False 创建, 使 token 对看门狗线程可见(否则看门狗无法续期)。
  • 看门狗线程按 GlobalLockManager 复用(非每锁一线程),由 Manager 在首次 acquire() 时懒启动,GlobalLockManager.close() 时统一停止;仅调用 acquire() 而不释放时,持有期间锁会持续续期。

设计原理

架构

GlobalLockManager (工厂)
    │
    ├─ RedisGlobalLock ────── redis.Redis + ConnectionPool (缓存)
    ├─ DjangoRedisGlobalLock ─ django_redis.get_redis_connection
    ├─ EtcdGlobalLock ─────── etcd3.client (每个实例独立)
    └─ ZookeeperGlobalLock ── KazooClient (缓存) + atexit 清理

连接池缓存

  • Redis 的 ConnectionPool 通过 @cacheutils.simple_cache 缓存,同一进程中所有 RedisGlobalLock 实例共享一个连接池。
  • ZooKeeper 的 KazooClient 同样缓存,所有 ZookeeperGlobalLock 实例共享一个客户端连接。

开发指南

启动测试服务

# 启动所有缺失的服务
bash scripts/start-test-services.sh

# 查看状态
bash scripts/start-test-services.sh status

# 停止所有服务
bash scripts/start-test-services.sh stop

# 使用 docker 而非 podman
CONTAINER_ENGINE=docker bash scripts/start-test-services.sh start

运行测试

pip3 install -r pytest.requirements.txt

# 单元测试(无需外部服务)
python3 -m pytest tests/test_base.py tests/test_config.py tests/test_implementations.py tests/test_security.py tests/test_django.py tests/test_redis_watchdog.py

# 集成测试(需要对应服务)
python3 -m pytest tests/test_integration_redis.py
python3 -m pytest tests/test_integration_etcd.py
python3 -m pytest tests/test_integration_zookeeper.py

# 全部测试
python3 -m pytest tests/

构建发布

pip3 install build twine
python3 -m build
python3 -m twine upload dist/*

测试通过版本

  • Python 3.6 ~ 3.12

发布历史

v0.2.0

  • Feature: Redis 引擎新增看门狗续期(watchdog / auto-renew)。由 GlobalLockManager 统一单看门狗线程按各锁自身的 renew_interval 周期续期(默认 5s),避免临界区较长时锁因 TTL 过期,进程崩溃后仍按 lock_ttl 自然回收。
  • Breaking: 锁参数 timeout 重命名为 lock_ttl(体现 Redis 锁 TTL 语义)。 旧参数 timeout 仍接受但被忽略,不再作为 TTL 使用。
  • Feature: lock_ttl/auto_renew/renew_interval 支持在 config(全局默认)或 lock() 参数(局部覆盖)中配置;Redis 引擎默认 lock_ttl=30、auto_renew=True、renew_interval=5。
  • Feature: 开启 auto_renew 时强制 redis.Lock 使用 thread_local=False,使看门狗线程可见主线程写入的 token,保证续期成功。
  • Feature: GlobalLockManager 新增统一生命周期管理:跟踪所有锁实例, close() / with 语法 / 进程退出(atexit)时统一释放仍持有的锁并停止看门狗。
  • Test: 新增看门狗续期单元测试(test_redis_watchdog.py)与 globallock.django 单元测试(test_django.py),Redis 引擎相关模块 行覆盖率 100%。

v0.1.5

  • Breaking: 改用 pyproject.toml 打包,移除 setup.py。
  • Bug fix: acquire() 现在会正确设置 is_locked 属性。
  • Bug fix: ZooKeeper _do_acquire 捕获 LockTimeout 返回 False,而非抛异常。
  • Bug fix: 修复 Django demo settings.py 中示例 app 被错误放在 MIDDLEWARE 的问题。
  • Bug fix: 修复 ZK 测试类名、docstring 复制粘贴错误。
  • Feature: 添加 KazooClient 进程退出自动关闭机制(atexit)。
  • Feature: 添加 __all__、类型注解。
  • Feature: 添加 key 前缀在各后端的一致性策略 (normalize_key_prefix_for_path)。
  • Feature: 添加 scripts/start-test-services.sh 一键启动测试服务。
  • Test: 新增 74 个单元测试(mock 方式,无需外部服务)。
  • Test: 新增 29 个集成测试(Redis/etcd/ZK 真实后端)。
  • Test: pytest markers 自动按服务可用性跳过 (redis_required, etcd_required, zookeeper_required)。
  • Security: 移除本地配置文件中硬编码的凭据。
  • Chore: 清理开发临时文件(t1.py, t1zk.py, t2etcd.py, t3redis.py)。

v0.1.4

  • 文档更新。

v0.1.3

  • 修正 globallock.django 默认设置。

v0.1.2

  • GlobalLockManager.lock() 方法参数可以在初始化时设置默认值。
  • 添加 globallock.django.get_default_global_lock_manager() 方法,允许在 Django 中便捷使用。

v0.1.1

  • 文档更新。

v0.1.0

  • 首次发布。
  • 支持 RedisGlobalLock、DjangoRedisGlobalLock、EtcdGlobalLock、ZookeeperGlobalLock。

Metadata

Release files for globallock 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for globallock 0.2.0
File Size Uploaded Provenance Actions
globallock-0.2.0.tar.gz 33.6 kB Without provenance Details for globallock-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for globallock 0.2.0
File Interpreter ABI Platform Actions
globallock-0.2.0-py3-none-any.whl Python 3 none any Details for globallock-0.2.0-py3-none-any.whl

Total release size: 52.6 kB

Release files / globallock-0.2.0.tar.gz

Download URL globallock-0.2.0.tar.gz
Size 33.6 kB
Tags Source
SHA-256 checksum
How to use checksums
36b508d1eb744e257d06b18f8c139ca7fceee323ca84642a85242d75ec188a5b
BLAKE2b-256 checksum
How to use checksums
d99a44232afe608528904fb1ee34c11e77a4fe080fd6917d35f4a247b8526b31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release files / globallock-0.2.0-py3-none-any.whl

Download URL globallock-0.2.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e5f24968b4d66b13b3b01e751e9f9dc0e9fa7180749f1a33bb252e4c4dd13623
BLAKE2b-256 checksum
How to use checksums
85df93f16ed9750cd95c2fc1606516c0fd041893315f0623cd0c6566756002d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Security for globallock 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page