dcfd24b624
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复 4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交 的 v1.0.2 全部工作。 v1.0.3 变更: Fixed: - generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播 - QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1) - OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀 - 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError Dependencies: - go mod tidy 补全 postgres 方言传递依赖;安全补丁升级 x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3 Removed: - Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName) - 死代码 database.DBResolver - 代码中 292 行"评分/理由"自夸注释(#26,23 个文件) Changed: - Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码 - database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入 - 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量 Security: - CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显 Added: - console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全) - 新增测试 jwt/repository/storage/database/router/middleware/app - 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志 - 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致) 详见 CHANGELOG.md 与 README.md 更新日志。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
304 lines
7.6 KiB
Go
304 lines
7.6 KiB
Go
package cache
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/database"
|
||
"github.com/EthanCodeCraft/xlgo-core/utils"
|
||
)
|
||
|
||
// 分布式锁错误
|
||
var (
|
||
ErrLockNotHeld = errors.New("锁未被当前客户端持有")
|
||
ErrLockExpired = errors.New("锁已过期")
|
||
ErrRedisNotReady = errors.New("Redis 未初始化")
|
||
)
|
||
|
||
// LockToken 锁令牌(用于安全释放锁)
|
||
type LockToken struct {
|
||
Key string // 锁的键名
|
||
Token string // 锁的唯一标识(UUID)
|
||
}
|
||
|
||
// lockScript 加锁 Lua 脚本
|
||
// 返回: 1 表示成功加锁,0 表示锁已被占用
|
||
const lockScript = `
|
||
if redis.call("exists", KEYS[1]) == 0 then
|
||
redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
|
||
return 1
|
||
else
|
||
return 0
|
||
end
|
||
`
|
||
|
||
// unlockScript 解锁 Lua 脯本
|
||
// 只有持有正确 Token 的客户端才能解锁
|
||
// 返回: 1 表示成功解锁,0 表示 Token 不匹配(锁不属于该客户端)
|
||
const unlockScript = `
|
||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||
redis.call("del", KEYS[1])
|
||
return 1
|
||
else
|
||
return 0
|
||
end
|
||
`
|
||
|
||
// extendScript 续期 Lua 脚本
|
||
// 只有持有正确 Token 的客户端才能续期
|
||
// 返回: 1 表示成功续期,0 表示 Token 不匹配或锁不存在
|
||
const extendScript = `
|
||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||
redis.call("pexpire", KEYS[1], ARGV[2])
|
||
return 1
|
||
else
|
||
return 0
|
||
end
|
||
`
|
||
|
||
// NewLock 创建分布式锁
|
||
// 参数: key 锁名称,ttl 锁定时长
|
||
// 返回: LockToken 用于后续解锁或续期
|
||
func NewLock(ctx context.Context, key string, ttl time.Duration) (*LockToken, error) {
|
||
if database.RedisClient == nil {
|
||
return nil, ErrRedisNotReady
|
||
}
|
||
|
||
token := utils.UUID()
|
||
ttlMs := int64(ttl / time.Millisecond)
|
||
|
||
result, err := database.RedisClient.Eval(ctx, lockScript, []string{key}, token, ttlMs).Result()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if result.(int64) == 1 {
|
||
return &LockToken{Key: key, Token: token}, nil
|
||
}
|
||
|
||
return nil, nil // 锁已被其他客户端持有
|
||
}
|
||
|
||
// Lock 简化的加锁函数(返回 bool)
|
||
// 注意: 使用此函数无法安全释放锁,建议使用 NewLock
|
||
func Lock(ctx context.Context, key string, ttl time.Duration) (bool, error) {
|
||
token, err := NewLock(ctx, key, ttl)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return token != nil, nil
|
||
}
|
||
|
||
// Unlock 安全释放锁
|
||
func Unlock(ctx context.Context, token *LockToken) error {
|
||
if database.RedisClient == nil {
|
||
return ErrRedisNotReady
|
||
}
|
||
|
||
if token == nil {
|
||
return ErrLockNotHeld
|
||
}
|
||
|
||
result, err := database.RedisClient.Eval(ctx, unlockScript, []string{token.Key}, token.Token).Result()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if result.(int64) == 0 {
|
||
return ErrLockNotHeld
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// UnlockByKey 按键名释放锁(不安全,仅用于旧代码兼容)
|
||
// 注意: 此函数不检查 Token,任何客户端都能释放锁
|
||
func UnlockByKey(ctx context.Context, key string) error {
|
||
if database.RedisClient == nil {
|
||
return nil
|
||
}
|
||
return database.RedisClient.Del(ctx, key).Err()
|
||
}
|
||
|
||
// ExtendLock 续期锁
|
||
// 参数: token 锁令牌,ttl 新的过期时间
|
||
func ExtendLock(ctx context.Context, token *LockToken, ttl time.Duration) error {
|
||
if database.RedisClient == nil {
|
||
return ErrRedisNotReady
|
||
}
|
||
|
||
if token == nil {
|
||
return ErrLockNotHeld
|
||
}
|
||
|
||
ttlMs := int64(ttl / time.Millisecond)
|
||
|
||
result, err := database.RedisClient.Eval(ctx, extendScript, []string{token.Key}, token.Token, ttlMs).Result()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if result.(int64) == 0 {
|
||
return ErrLockNotHeld
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// TryLock 尝试获取锁,失败时等待重试
|
||
func TryLock(ctx context.Context, key string, ttl time.Duration, retryInterval time.Duration, maxRetry int) (*LockToken, error) {
|
||
for i := 0; i < maxRetry; i++ {
|
||
token, err := NewLock(ctx, key, ttl)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if token != nil {
|
||
return token, nil
|
||
}
|
||
time.Sleep(retryInterval)
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// WithLock 使用分布式锁执行函数(自动管理锁)
|
||
// 参数: key 锁名称,ttl 锁定时长,fn 业务函数
|
||
// 注意: 如果任务执行时间超过 ttl,需要设置更长的 ttl 或使用 WithLockAutoExtend
|
||
func WithLock(ctx context.Context, key string, ttl time.Duration, fn func() error) error {
|
||
token, err := NewLock(ctx, key, ttl)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if token == nil {
|
||
return nil // 未获取到锁,跳过执行
|
||
}
|
||
defer Unlock(ctx, token)
|
||
|
||
return fn()
|
||
}
|
||
|
||
// WithLockAutoExtend 使用分布式锁执行函数(自动续期)
|
||
// 参数: key 锁名称,initialTTL 初始锁定时长,extendInterval 续期间隔,fn 业务函数
|
||
func WithLockAutoExtend(ctx context.Context, key string, initialTTL time.Duration, extendInterval time.Duration, fn func() error) error {
|
||
token, err := NewLock(ctx, key, initialTTL)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if token == nil {
|
||
return nil // 未获取到锁,跳过执行
|
||
}
|
||
|
||
// 启动续期协程
|
||
done := make(chan struct{})
|
||
go func() {
|
||
defer close(done)
|
||
ticker := time.NewTicker(extendInterval)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-done:
|
||
return
|
||
case <-ticker.C:
|
||
// 续期锁(每次续期为 initialTTL)
|
||
if err := ExtendLock(ctx, token, initialTTL); err != nil {
|
||
return // 续期失败,停止续期
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
|
||
// 执行业务函数
|
||
err = fn()
|
||
|
||
// 停止续期并释放锁
|
||
done <- struct{}{}
|
||
Unlock(ctx, token)
|
||
|
||
return err
|
||
}
|
||
|
||
// IsLocked 检查锁是否被占用(不获取锁)
|
||
func IsLocked(ctx context.Context, key string) (bool, error) {
|
||
if database.RedisClient == nil {
|
||
return false, nil
|
||
}
|
||
return database.RedisClient.Exists(ctx, key).Val() > 0, nil
|
||
}
|
||
|
||
// GetLockTTL 获取锁的剩余过期时间
|
||
func GetLockTTL(ctx context.Context, key string) (time.Duration, error) {
|
||
if database.RedisClient == nil {
|
||
return 0, nil
|
||
}
|
||
return database.RedisClient.TTL(ctx, key).Result()
|
||
}
|
||
|
||
// ForceUnlock 强制释放锁(危险操作,仅用于管理场景)
|
||
// 注意: 此函数不检查 Token,强制删除锁
|
||
func ForceUnlock(ctx context.Context, key string) error {
|
||
if database.RedisClient == nil {
|
||
return nil
|
||
}
|
||
return database.RedisClient.Del(ctx, key).Err()
|
||
}
|
||
|
||
// ===== 计数器操作 =====
|
||
|
||
// Incr 自增计数器
|
||
func Incr(ctx context.Context, key string) (int64, error) {
|
||
if database.RedisClient == nil {
|
||
return 0, nil
|
||
}
|
||
return database.RedisClient.Incr(ctx, key).Result()
|
||
}
|
||
|
||
// IncrBy 指定增量自增
|
||
func IncrBy(ctx context.Context, key string, value int64) (int64, error) {
|
||
if database.RedisClient == nil {
|
||
return 0, nil
|
||
}
|
||
return database.RedisClient.IncrBy(ctx, key, value).Result()
|
||
}
|
||
|
||
// Decr 自减计数器
|
||
func Decr(ctx context.Context, key string) (int64, error) {
|
||
if database.RedisClient == nil {
|
||
return 0, nil
|
||
}
|
||
return database.RedisClient.Decr(ctx, key).Result()
|
||
}
|
||
|
||
// GetTTL 获取键的剩余过期时间
|
||
func GetTTL(ctx context.Context, key string) (time.Duration, error) {
|
||
if database.RedisClient == nil {
|
||
return 0, nil
|
||
}
|
||
return database.RedisClient.TTL(ctx, key).Result()
|
||
}
|
||
|
||
// SetExpire 设置键的过期时间
|
||
func SetExpire(ctx context.Context, key string, ttl time.Duration) (bool, error) {
|
||
if database.RedisClient == nil {
|
||
return false, nil
|
||
}
|
||
return database.RedisClient.Expire(ctx, key, ttl).Result()
|
||
}
|
||
|
||
// GetRaw 获取原始字符串值(不反序列化)
|
||
func GetRaw(ctx context.Context, key string) (string, error) {
|
||
if database.RedisClient == nil {
|
||
return "", nil
|
||
}
|
||
return database.RedisClient.Get(ctx, key).Result()
|
||
}
|
||
|
||
// SetRaw 设置原始值(不序列化)
|
||
func SetRaw(ctx context.Context, key string, value string, ttl time.Duration) error {
|
||
if database.RedisClient == nil {
|
||
return nil
|
||
}
|
||
return database.RedisClient.Set(ctx, key, value, ttl).Err()
|
||
} |