distlock

package
v0.46.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package distlock 提供分布式锁的**后端中立契约单源**(etcdx#123 治本侧): Locker 接口、哨兵错误集与 WithLock 便捷包装在此定义一次,redisx/distlock 与 etcdx/distlock 后续波次改为消费本包——同一份代码不再在两个后端仓各持 一份、靠注释互相提醒手工对账。

本包**零依赖**(连 oekit 内部也不依赖,pkg 仓红线):只有接口、哨兵与 纯标准库的 WithLock 骨架,不含任何 Redis/etcd 语义。

哨兵错误集(以 redisx/distlock 文案为基准)

  • ErrLockNotAcquired "lock not acquired"——锁被他人持有(TryLock 单次 未抢到,不是故障);
  • ErrLockTimeout "lock acquire timeout"——Lock 在 timeout 内未抢到;
  • ErrLockNotHeld "lock not held by this owner"——Unlock 时锁不存在或 已易主,本次**没有删除任何锁**(调用方不得继续假装持有)。

文案漂移记录(etcdx#123 的核心痛点):etcdx/distlock 历史上的 ErrNotOwner 文案是 "lock not owned by this owner",与 redisx 侧的 ErrLockNotHeld 同语义不同文案——两仓各自 errors.New 出的哨兵是不同的值, errors.Is 跨包恒 false,适配层换后端后错误分类随之漂移(429 变 500), etcdx 为此打了两层补丁(sentinel.Is 同文案等价 + IsOneOf 双向判定)。 契约单源后哨兵只有一个值,errors.Is 即正确分类,两套补丁机制不再有 存在必要。wave 2 改造时:etcdx 的 ErrNotOwner 语义统一映射到本包 ErrLockNotHeld(读侧若需兼容旧文案,由 etcdx 自己的适配层负责翻译)。

WithLock 释放纪律(单源口径,采 redisx 的 context.WithoutCancel)

持锁期 caller ctx 取消/超时**不吞释放动作**:解锁用与 fn 的取消状态解耦 的 context(WithoutCancel)——fn 执行期间 ctx 到期时,取消的 ctx 会让 后端发解锁命令前就返回 context.Canceled,DEL/租约撤销永不执行,锁变孤儿, 其他实例阻塞到 TTL 过期。etcdx 历史实现用 WithTimeout(Background, 3s), 是同一诉求的另一形态;单源采 WithoutCancel(较新纪律,无魔法常数)。 释放失败只记日志(log/slog,component=distlock)不覆盖 fn 的返回值—— TTL/租约到期是后端的安全网,失败要可观测但不必把业务错误顶掉。

后端实现接入指引(redisx / etcdx wave 2 改造照此执行)

  1. 接口:后端的 Locker 实现**直接实现本包 Locker**(方法集逐参数同形, 两仓现状已满足;redisx 的 Renew、etcdx 的 MaxHold 等后端特有能力 留在各自具体类型/构造选项上,不进中立接口——可选能力走接口断言, 参考 cache 包 Sweeper/PeekGetter 的形态)。
  2. 哨兵:后端包删除自己的 errors.New 哨兵,包级变量别名指向本包 (`var ErrLockTimeout = distlock.ErrLockTimeout`——同类型同值, 下游 errors.Is 两侧任一写法都命中),同进程只此一套值。
  3. WithLock:后端包的 WithLock 改为薄委托——把 Locker 实现传进来即可 (签名兼容,调用方零改动);带 per-instance logger 等增强的 (redisx#120),后端可自行组合 Lock + ReleaseUncancelable 骨架, 释放纪律仍走单源。
  4. ttl/timeout 等参数校验口径(ttl 必须为正、timeout<=0 单次尝试等) 属后端行为差异记录在各自 Locker 文档,本包不做运行时校验——契约层 只定形状与哨兵,语义差异逐条声明不装作不存在(etcdx#86 的教训)。

用法(业务侧)

var l distlock.Locker = redisxdistlock.NewRedisLocker(cli) // wave 2 起
err := distlock.WithLock(ctx, l, "job:nightly", 30*time.Second, 5*time.Second, func() error {
    return runNightlyJob()
})
if errors.Is(err, distlock.ErrLockTimeout) { /* 429 / 不重试 */ }

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrLockNotAcquired 表示锁被其他实例持有,获取失败(TryLock 单次
	// 未抢到,不是通信故障)。
	ErrLockNotAcquired = errors.New("lock not acquired")

	// ErrLockTimeout 表示在指定超时时间内未能获取锁。
	ErrLockTimeout = errors.New("lock acquire timeout")

	// ErrLockNotHeld 表示解锁时锁不存在或已易主——本次 Unlock **没有删除
	// 任何锁**。典型成因:锁 TTL 过期后已被他人重新获取、或对同一
	// (key, ownerID) 重复 Unlock。收到本哨兵即说明自己已不再持有锁,
	// 不得继续在临界区内工作(继续执行会与新持有者并发);errors.Is 判定,
	// 与其它后端通信错误(可重试)区分。
	ErrLockNotHeld = errors.New("lock not held by this owner")
)

哨兵错误集(etcdx#123 单源;文案以 redisx/distlock 为基准,漂移史与 etcdx 侧 ErrNotOwner 的映射关系见包文档)。同进程只此一套值——后端仓 wave 2 起别名指向这里,不再各自 errors.New。

Functions

func ReleaseUncancelable

func ReleaseUncancelable(ctx context.Context, locker Locker, key, ownerID string) error

ReleaseUncancelable 用与 caller ctx 取消状态解耦的 context 释放锁并记录 失败(WithLock 与后端包自组 WithLock 共用的释放纪律单源)。

为什么要 WithoutCancel:fn 执行期间 ctx 到期(timeout 参数存在的意义)时, 取消的 ctx 会让后端在发解锁命令前就返回 context.Canceled,DEL/租约撤销 永不执行——锁变孤儿,其他实例阻塞到 TTL 过期。ctx 的取值(鉴权等)原样 保留,只剥离取消信号(context.WithoutCancel,Go 1.21+)。

释放失败记 Warn(component=distlock)并返回错误;调用方(WithLock)只记 日志不覆盖 fn 结果,需要区分处理的场景可直接消费返回值。

func WithLock

func WithLock(ctx context.Context, locker Locker, key string, ttl, timeout time.Duration, fn func() error) error

WithLock 是便捷包装:加锁 → 执行 fn → 自动释放锁(etcdx#123 的单源实现, redisx/etcdx 两仓的 WithLock wave 2 改为薄委托本函数)。

使用 Lock(带超时重试)获取锁;获取失败(含 ErrLockTimeout)直接返回 错误、fn 不执行。释放走 ReleaseUncancelable:**持锁期 caller ctx 取消 不吞释放动作**(WithoutCancel 语义与历史背景见包文档)。释放失败只记 日志不覆盖 fn 的返回值——分布式锁释放失败可能导致孤儿锁直到 TTL 过期, 记录日志便于运维定位,但业务结果不该被释放故障顶掉(租约/TTL 到期是 后端安全网)。

Types

type Locker

type Locker interface {
	// TryLock 非阻塞尝试获取锁。
	// 返回 (ownerID, nil) 表示成功,ownerID 用于后续 Unlock。
	// 返回 ("", ErrLockNotAcquired) 表示锁被占用。
	TryLock(ctx context.Context, key string, ttl time.Duration) (string, error)

	// Lock 阻塞获取锁,带超时重试。
	// 返回 (ownerID, nil) 表示成功。
	// 返回 ("", ErrLockTimeout) 表示超时未获取到。
	Lock(ctx context.Context, key string, ttl time.Duration, timeout time.Duration) (string, error)

	// Unlock 释放锁,ownerID 必须匹配。
	// 返回 nil 表示锁已删除;锁不存在或已易主(陈旧持有者解锁)返回
	// ErrLockNotHeld(调用方不得继续假装持有);其它 error 为后端通信故障。
	Unlock(ctx context.Context, key string, ownerID string) error
}

Locker 是分布式锁的后端中立接口(etcdx#123 契约单源;redisx/distlock 与 etcdx/distlock 的历史接口方法集本就同形,此处取两仓接口并集)。

实现约定:

  • ownerID 是本次持有的凭据(随机串或租约 ID),Unlock 必须校验匹配—— 「只有持有者能释放」是分布式锁的正确性底线;
  • 后端特有能力(自动续期、最大持锁时长等)留在具体类型/构造选项上, 不进中立接口;需要可判定的可选能力时按接口断言外置(参考 cache 包 Sweeper/PeekGetter 的形态);
  • ttl / timeout 的边界语义(ttl 必须为正、timeout<=0 单次尝试等)由 各后端在自己的文档逐条声明(两仓现状口径见各自 Locker 注释),本 接口只定形状与哨兵,不装作差异不存在(etcdx#86)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL