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 改造照此执行) ¶
- 接口:后端的 Locker 实现**直接实现本包 Locker**(方法集逐参数同形, 两仓现状已满足;redisx 的 Renew、etcdx 的 MaxHold 等后端特有能力 留在各自具体类型/构造选项上,不进中立接口——可选能力走接口断言, 参考 cache 包 Sweeper/PeekGetter 的形态)。
- 哨兵:后端包删除自己的 errors.New 哨兵,包级变量别名指向本包 (`var ErrLockTimeout = distlock.ErrLockTimeout`——同类型同值, 下游 errors.Is 两侧任一写法都命中),同进程只此一套值。
- WithLock:后端包的 WithLock 改为薄委托——把 Locker 实现传进来即可 (签名兼容,调用方零改动);带 per-instance logger 等增强的 (redisx#120),后端可自行组合 Lock + ReleaseUncancelable 骨架, 释放纪律仍走单源。
- 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 ¶
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 ¶
ReleaseUncancelable 用与 caller ctx 取消状态解耦的 context 释放锁并记录 失败(WithLock 与后端包自组 WithLock 共用的释放纪律单源)。
为什么要 WithoutCancel:fn 执行期间 ctx 到期(timeout 参数存在的意义)时, 取消的 ctx 会让后端在发解锁命令前就返回 context.Canceled,DEL/租约撤销 永不执行——锁变孤儿,其他实例阻塞到 TTL 过期。ctx 的取值(鉴权等)原样 保留,只剥离取消信号(context.WithoutCancel,Go 1.21+)。
⚠️ 上界与不可取消不互斥(#428):WithoutCancel 只剥取消信号,**deadline 一并剥掉了**——裸 WithoutCancel 的 Unlock 永不超时。本函数在其上叠 WithTimeout(releaseTimeout) 补回释放动作的硬上界;超时返回可 errors.Is(err, context.DeadlineExceeded) 判定的错误。
释放失败记 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)。