package common import ( "context" "errors" "fmt" "time" "github.com/gogf/gf/v2/database/gredis" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcache" ) // ErrLockHeld 锁被他人持有(重试耗尽仍拿不到时返回) var ErrLockHeld = errors.New("lock held") // WithLock 互斥临界区唯一入口(泛型):业务返回值经 T 原样透出。 // 内部按 config.yml 自动选择锁实现:配置了 redis 节点 → redis 锁(跨实例互斥, // SET NX EX + token 对比删除防误删他人锁);未配置 → gcache 内存锁(单实例互斥)。 // 拿不到锁最多重试 retries 次、每次间隔 retryInterval(retries=0 立即失败; // ctx 取消/超时同样终止);中间件故障不重试直接返回。defer 自动释放:无论 fn // 成功、失败还是 panic。expire 必须 > 0(进程崩溃兜底不死锁),fn 耗时须在 expire 前完成, // fn 内禁止长耗时 IO(LLM/DB 调用);锁粒度按业务唯一键尽量小。 func WithLock[T any](ctx context.Context, key string, expire time.Duration, retries int, retryInterval time.Duration, fn func() (T, error)) (T, error) { var zero T if expire <= 0 { return zero, errors.New("lock expire must be positive") } lock, err := newLock(ctx, key, expire) if err != nil { return zero, err } var ok bool for attempt := 0; ; attempt++ { ok, err = lock.TryAcquire(ctx) if err != nil { return zero, err } if ok { break } if attempt >= retries { return zero, ErrLockHeld } select { case <-ctx.Done(): return zero, ctx.Err() case <-time.After(retryInterval): } } defer lock.Release(ctx) return fn() } type lock interface { TryAcquire(ctx context.Context) (bool, error) Release(ctx context.Context) } func newLock(ctx context.Context, key string, expire time.Duration) (lock, error) { token := fmt.Sprintf("%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%1e9) if g.Cfg().MustGet(ctx, "redis.default.address", "").String() != "" { return &redisLock{key: "lock:" + key, token: token, expire: expire, client: g.Redis()}, nil } return &memoryLock{key: "lock:" + key, token: token, expire: expire}, nil } type redisLock struct { key string token string expire time.Duration client *gredis.Redis } func (l *redisLock) TryAcquire(ctx context.Context) (bool, error) { // SetNX 无 TTL 参数,SET NX 与 TTL 分两步;当前项目未配置 redis 节点此路径不可达, // 若崩溃于两步之间仅残留无 TTL 锁(token 归属明确,可手工清除),可接受 ok, err := l.client.SetNX(ctx, l.key, l.token) if err != nil || !ok { return ok, err } if _, err := l.client.PExpire(ctx, l.key, l.expire.Milliseconds()); err != nil { if _, derr := l.client.Del(ctx, l.key); derr != nil { g.Log().Warningf(ctx, "清理未设 TTL 的锁失败: %v", derr) } return false, err } return true, nil } func (l *redisLock) Release(ctx context.Context) { v, err := l.client.Get(ctx, l.key) if err != nil { g.Log().Warningf(ctx, "释放锁失败(读取): %v", err) return } if v.String() == l.token { if _, err := l.client.Del(ctx, l.key); err != nil { g.Log().Warningf(ctx, "释放锁失败(删除): %v", err) } } } type memoryLock struct { key string token string expire time.Duration } func (l *memoryLock) TryAcquire(ctx context.Context) (bool, error) { return gcache.SetIfNotExist(ctx, l.key, l.token, l.expire) } func (l *memoryLock) Release(ctx context.Context) { v, err := gcache.Get(ctx, l.key) if err != nil { g.Log().Warningf(ctx, "释放锁失败(读取): %v", err) return } if v.String() == l.token { if _, err := gcache.Remove(ctx, l.key); err != nil { g.Log().Warningf(ctx, "释放锁失败(删除): %v", err) } } }