Files
server/app/utils/lock_helper.go
T
2026-01-16 15:49:34 +08:00

206 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"context"
"fmt"
"time"
"github.com/goravel/framework/facades"
"github.com/redis/go-redis/v9"
)
// LockResult 锁操作结果
type LockResult struct {
Acquired bool // 是否成功获取锁
Error error // 错误信息
Client *redis.Client // Redis 客户端(如果使用 Redis
}
// TryAcquireLock 尝试获取分布式锁(原子操作)
// lockKey: 锁的键名
// lockValue: 锁的值(用于标识锁的拥有者)
// ttl: 锁的过期时间
// 返回 LockResult,包含是否成功获取锁和错误信息
func TryAcquireLock(lockKey, lockValue string, ttl time.Duration) *LockResult {
result := &LockResult{
Acquired: false,
}
// 优先使用 Redis SETNX 原子操作
redisClient, err := GetRedisClient("default")
if err != nil {
facades.Log().Warningf("获取 Redis 客户端失败,降级到缓存锁: %v", err)
// 降级到普通缓存检查(不保证原子性,但至少提供基本保护)
return tryAcquireLockWithCache(lockKey, lockValue, ttl)
}
// 使用 Redis SETNX 原子操作(SET key value NX EX seconds
// SetNX 会自动设置过期时间,如果键已存在且未过期,返回 false
ctx := context.Background()
acquired, err := redisClient.SetNX(ctx, lockKey, lockValue, ttl).Result()
if err != nil {
facades.Log().Errorf("Redis 获取锁失败: key=%s, error=%v", lockKey, err)
result.Error = fmt.Errorf("获取锁失败: %v", err)
// 注意:使用公共 Redis 客户端,不需要手动关闭
return result
}
if !acquired {
// 锁已被占用,检查锁的剩余过期时间
ttlResult, _ := redisClient.TTL(ctx, lockKey).Result()
if ttlResult <= 0 {
// 锁已过期,删除它并重试
redisClient.Del(ctx, lockKey)
// 重试一次
acquired, err = redisClient.SetNX(ctx, lockKey, lockValue, ttl).Result()
if err != nil {
facades.Log().Errorf("Redis 重试获取锁失败: key=%s, error=%v", lockKey, err)
result.Error = fmt.Errorf("获取锁失败: %v", err)
// 注意:使用公共 Redis 客户端,不需要手动关闭
return result
}
}
}
result.Acquired = acquired
if acquired {
result.Client = redisClient // 只有获取成功才保留客户端
} else {
result.Client = nil
}
return result
}
// ReleaseLock 释放锁
// 注意:只有锁的拥有者才能释放锁(通过 lockValue 验证)
func ReleaseLock(lockKey, lockValue string, client *redis.Client) error {
if client == nil {
// 如果没有 Redis 客户端,使用缓存删除
_ = facades.Cache().Forget(lockKey)
return nil
}
ctx := context.Background()
// 使用 Lua 脚本确保只有锁的拥有者才能释放锁
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
_, err := client.Eval(ctx, script, []string{lockKey}, lockValue).Result()
if err != nil {
// 如果 Lua 脚本执行失败,尝试直接删除(降级处理)
return client.Del(ctx, lockKey).Err()
}
return nil
}
// CloseLockClient 关闭锁的 Redis 客户端
// 注意:由于现在使用公共 Redis 客户端池,通常不需要手动关闭
// 此函数保留用于兼容性,但不会真正关闭客户端(客户端由连接池管理)
func CloseLockClient(client *redis.Client) {
// 使用公共 Redis 客户端池,不需要手动关闭
// 客户端由连接池统一管理
}
// tryAcquireLockWithCache 使用缓存实现锁(降级方案,不保证原子性)
// 注意:由于缓存操作的检查-设置不是原子的,在高并发下可能仍有竞态条件
// 但至少可以提供基本的保护
func tryAcquireLockWithCache(lockKey, lockValue string, ttl time.Duration) *LockResult {
result := &LockResult{
Acquired: false,
}
// 检查是否已有锁
var cachedValue string
cacheErr := facades.Cache().Get(lockKey, &cachedValue)
// 如果 Get 返回 nil(成功)且值不为空,说明锁已存在
if cacheErr == nil && cachedValue != "" {
// 锁已存在
result.Error = fmt.Errorf("锁已被占用")
return result
}
// 设置锁(使用 Put,如果键已存在会覆盖,但至少可以防止大部分重复请求)
if err := facades.Cache().Put(lockKey, lockValue, ttl); err != nil {
facades.Log().Errorf("设置缓存锁失败: key=%s, error=%v", lockKey, err)
result.Error = fmt.Errorf("设置锁失败: %v", err)
return result
}
// 再次检查,确保锁设置成功(双重检查,减少竞态条件的影响)
// 短暂延迟,让其他并发请求有机会检测到锁
time.Sleep(50 * time.Millisecond)
var verifyValue string
if facades.Cache().Get(lockKey, &verifyValue) == nil {
if verifyValue == lockValue {
// 锁设置成功且值匹配
result.Acquired = true
return result
}
// 值不匹配,说明被其他请求覆盖了(竞态条件)
result.Error = fmt.Errorf("锁已被占用")
return result
}
// 锁设置后无法验证,可能是缓存问题
result.Error = fmt.Errorf("设置锁失败")
return result
}
// LockGuard 锁保护器,自动管理锁的生命周期
type LockGuard struct {
lockKey string
lockValue string
client *redis.Client
acquired bool
}
// AcquireLock 获取锁
// lockKey: 锁的键名(会自动添加用户ID前缀,格式:lockKey:userID
// userID: 用户ID(用于生成唯一的锁键)
// ttl: 锁的过期时间
// 返回 LockGuard 和错误,如果锁已被占用,返回 ErrLockAcquired 错误
func AcquireLock(lockKey string, userID uint, ttl time.Duration) (*LockGuard, error) {
// 生成完整的锁键和值
fullLockKey := fmt.Sprintf("%s:%d", lockKey, userID)
lockValue := fmt.Sprintf("%d_%d", userID, time.Now().Unix())
// 尝试获取锁
lockResult := TryAcquireLock(fullLockKey, lockValue, ttl)
if lockResult.Error != nil {
return nil, lockResult.Error
}
if !lockResult.Acquired {
return nil, fmt.Errorf("锁已被占用")
}
return &LockGuard{
lockKey: fullLockKey,
lockValue: lockValue,
client: lockResult.Client,
acquired: true,
}, nil
}
// Release 释放锁
func (g *LockGuard) Release() {
if !g.acquired {
return
}
if g.client != nil {
if err := ReleaseLock(g.lockKey, g.lockValue, g.client); err != nil {
facades.Log().Errorf("释放 Redis 锁失败: key=%s, error=%v", g.lockKey, err)
}
CloseLockClient(g.client)
g.client = nil // 防止重复关闭
} else {
// 使用缓存时,直接删除
_ = facades.Cache().Forget(g.lockKey)
}
g.acquired = false
}