feat(cache):修复redis永久key问题
This commit is contained in:
1 parent
479340d4ea
commit
16c50b2875
6 files changed
+27
-20
No files matched your search
@@ -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 获取租户并发上限。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user