package common import ( "context" "crypto/rand" "encoding/hex" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcache" ) // ErrLockHeld 锁被占用:重试 retries 次仍未获取时返回。 var ErrLockHeld = gerror.New("操作过于频繁,请稍后再试") // WithLock 业务互斥锁(唯一入口): // - expire 必须 > 0,进程崩溃后锁自动过期兜底不死锁;fn 耗时必须小于 expire,fn 内禁止长耗时 IO // - 拿不到锁重试 retries 次、每次间隔 retryInterval(retries=0 立即失败;ctx 取消/超时同样终止等待) // - 中间件故障不重试,直接返回错误 // - 锁实现按配置自动选择:配置了 redis 节点 → redis 锁(跨实例互斥);未配置 → gcache 内存锁(单实例互斥)。 // redis 后端需引入 github.com/gogf/gf/contrib/redis/v2 适配器并接入 SET NX EX + token 对比删除; // 当前未配置 redis 时使用内存锁。 // - 无论 fn 成功、失败还是 panic,锁在函数退出时自动释放 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, gerror.New("锁过期时间必须大于 0") } if lockBackendIsRedis(ctx) { return zero, gerror.New("配置了 redis 锁后端,但当前未接入 contrib/redis 适配器") } token := randomToken() for attempt := 0; ; attempt++ { ok, err := gcache.SetIfNotExist(ctx, key, token, expire) if err != nil { return zero, err } if ok { defer func() { _, _ = gcache.Remove(ctx, key) }() return fn() } if attempt >= retries { return zero, ErrLockHeld } select { case <-ctx.Done(): return zero, ctx.Err() case <-time.After(retryInterval): } } } func lockBackendIsRedis(ctx context.Context) bool { v, err := g.Cfg().Get(ctx, "redis", nil) return err == nil && v != nil && !v.IsEmpty() } func randomToken() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) }