feat(cache):修复redis永久key问题

This commit is contained in:
qian committed 2026-06-04 07:48:28 +08:00
1 parent 479340d4ea
commit 16c50b2875
6 files changed
+27 -20

No files matched your search

+6 -1
View File
@@ -201,6 +201,7 @@ local function tryAcquire(i)
local v = redis.call("DECR", acquired[j])
if v < 0 then
redis.call("SET", acquired[j], 0)
redis.call("EXPIRE", acquired[j], expire)
end
end
return false
@@ -217,11 +218,14 @@ return 1
`
// releaseConcurrentLua 一次 EVAL 释放 4 级并发许可。
// ARGV[1] = expire 秒数(用于 SET 后恢复 TTL)
var releaseConcurrentLua = `
local expire = tonumber(ARGV[1])
for i = 1, 4 do
local v = redis.call("DECR", KEYS[i])
if v < 0 then
redis.call("SET", KEYS[i], 0)
redis.call("EXPIRE", KEYS[i], expire)
end
end
return 1
@@ -261,7 +265,8 @@ func ReleaseConcurrent(ctx context.Context, tenantID, userID, apiKeyID int64, mo
keyKey := fmt.Sprintf("conc:key:%d", apiKeyID)
g.Redis().Do(ctx, "EVAL", releaseConcurrentLua, 4,
tenantKey, modelKey, userKey, keyKey)
tenantKey, modelKey, userKey, keyKey,
300) // EXPIRE 秒数
}
// getTenantConcurrencyLimit 获取租户并发上限。
+5 -1
View File
@@ -160,7 +160,9 @@ redis.call("HSET", prededuct_key, "amount", amount)
redis.call("HSET", prededuct_key, "tenant_id", ARGV[4])
redis.call("HSET", prededuct_key, "model_name", ARGV[5])
redis.call("HSET", prededuct_key, "created_at", ARGV[6])
redis.call("SADD", "prededuct_active:" .. ARGV[4], request_id)
local active_set = "prededuct_active:" .. ARGV[4]
redis.call("SADD", active_set, request_id)
redis.call("EXPIRE", active_set, 30 * 86400)
redis.call("EXPIRE", prededuct_key, ttl)
return 1
`
@@ -510,6 +512,8 @@ func rebuildPredeductFromDB(ctx context.Context, tenantID int64) {
g.Redis().Do(ctx, "EXPIRE", predeductKey, remainingTTL)
g.Redis().Do(ctx, "SADD", activeSetKey, t.RequestID)
}
// 确保 active SET 有 TTL(30 天),过期后下次预扣时自动重建
g.Redis().Do(ctx, "EXPIRE", activeSetKey, 30*86400)
g.Log().Infof(ctx, "[PRE-DEDUCT] rebuilt %d active tracks from DB for tenant=%d", len(tracks), tenantID)
}
+3 -1
View File
@@ -57,7 +57,9 @@ func (c *Cache) Set(ctx context.Context, key string, value any, ttl ...time.Dura
if ttlSeconds > 0 {
_, _ = g.Redis().Do(ctx, "SETEX", fullKey, ttlSeconds, string(jsonBytes))
} else {
_, _ = g.Redis().Do(ctx, "SET", fullKey, string(jsonBytes))
// 兜底:ttl ≤ 0 时使用 24h 兜底 TTL,避免产生永久 key
g.Log().Warningf(ctx, "[Cache] TTL <= 0 for key=%s, using fallback 24h TTL", fullKey)
_, _ = g.Redis().Do(ctx, "SETEX", fullKey, 86400, string(jsonBytes))
}
}
+8 -11
View File
@@ -44,21 +44,18 @@ func SendVerifyCode(ctx context.Context, email string, purpose VerifyPurpose) er
fmt.Sprintf("请 %d 秒后再试", secs))
}
// Rate limit: max 5 per hour
// Rate limit: max 5 per hour (atomic Lua: INCR + first-time EXPIRE)
hourlyKey := fmt.Sprintf("verify:hourly:%s:%s", email, purpose)
hourlyCount, err := g.Redis().Do(ctx, "INCR", hourlyKey)
hourlyCount, err := g.Redis().Do(ctx, "EVAL",
`local count = redis.call("INCR", KEYS[1])
if count == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return count`,
1, hourlyKey, 3600)
if err == nil && hourlyCount.Int() > 5 {
return NewBusinessError(consts.CodeRateLimitExceeded, "每小时最多发送5次验证码")
}
// Set the 1-hour window TTL only when it is missing. This keeps a fixed
// window (not a sliding one) while still repairing an orphaned key left by
// a crash between INCR and EXPIRE (TTL < 0 means no expiry or key absent).
if ttl, err := g.Redis().Do(ctx, "TTL", hourlyKey); err == nil && ttl.Int() < 0 {
if _, err := g.Redis().Do(ctx, "EXPIRE", hourlyKey, 3600); err != nil {
g.Log().Warningf(ctx, "failed to set hourly rate limit TTL: %v", err)
}
}
// Generate 6-digit code
code, err := generateCode(6)
if err != nil {
+1 -2
View File
@@ -85,11 +85,10 @@ func (s *sTenant) GetOAuthAuthorizeURL(ctx context.Context, req *v1.OAuthAuthori
redirectURI := siteURL + fmt.Sprintf("/api/tenant/oauth/%s/callback", req.Provider)
state := generateState()
_, err := g.Redis().Set(ctx, oauthStatePrefix+state, req.Provider)
_, err := g.Redis().Do(ctx, "SET", oauthStatePrefix+state, req.Provider, "EX", int64(oauthStateTTL.Seconds()))
if err != nil {
return nil, err
}
_, _ = g.Redis().Expire(ctx, oauthStatePrefix+state, int64(oauthStateTTL.Seconds()))
authorizeURL := provider.GetAuthorizeURL(clientID, redirectURI, state)
return &v1.OAuthAuthorizeRes{
+4 -4
View File
@@ -143,8 +143,8 @@ func (s *sTenant) OpenAppCreate(ctx context.Context, req *v1.OpenAppCreateReq) (
id, _ := result.LastInsertId()
// Store encrypted secret in Redis — 失败时回滚 DB 记录
_, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", id), encryptedSecret)
// Store encrypted secret in Redis(30 天 TTL,过期后从 DB 重新加载)— 失败时回滚 DB 记录
_, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", id), encryptedSecret, "EX", 30*86400)
if err != nil {
// 回滚:删除已插入的 DB 记录
_, _ = dao.OpnApps.Ctx(ctx).Where("id", id).Delete()
@@ -235,8 +235,8 @@ func (s *sTenant) OpenAppResetSecret(ctx context.Context, req *v1.OpenAppResetSe
return nil, err
}
// 先写 Redis,成功后再更新 DB
_, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", req.Id), encryptedSecret)
// 先写 Redis(30 天 TTL),成功后再更新 DB
_, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", req.Id), encryptedSecret, "EX", 30*86400)
if err != nil {
return nil, fmt.Errorf("update app secret in Redis failed: %w", err)
}