choose 校验节点/选项归属与解锁链,终局在 common.WithLock 内按 技术设计 4.3/4.4 结算:首次到达才结算、连续失败限扣、余额不为负、 完美集齐全终局 +20 并解锁计策卡与下一计;路径流水尽力而为。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
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)
|
||
}
|