Files
new-api/model/quota_reserve.go
T

69 lines
2.1 KiB
Go

package model
import (
"context"
"github.com/QuantumNous/new-api/common"
)
type cacheQuotaResult int
const (
cacheQuotaInsufficient cacheQuotaResult = iota
cacheQuotaOK
cacheQuotaMiss
)
// 下列脚本都是守卫式的:只在完整哈希(Id 匹配且配额字段存在)上操作,
// 哈希缺失时返回 miss 而不是创建残缺哈希。脚本不修改 TTL(HINCRBY 天然保留
// 水合时设置的 TTL),因此即使某个写库路径绕过了缓存,偏差也会在一个 TTL
// 窗口内随缓存过期而自愈。
const userQuotaReserveScript = `
if tonumber(redis.call('HGET', KEYS[1], 'Id') or '0') ~= tonumber(ARGV[2])
or tonumber(redis.call('HGET', KEYS[1], 'CacheSchema') or '0') ~= tonumber(ARGV[3])
or redis.call('HEXISTS', KEYS[1], 'Quota') == 0 then
return -1
end
local quota = tonumber(redis.call('HGET', KEYS[1], 'Quota'))
if quota == nil or quota < tonumber(ARGV[1]) then
return 0
end
redis.call('HINCRBY', KEYS[1], 'Quota', -tonumber(ARGV[1]))
return 1`
const userQuotaDeltaScript = `
if tonumber(redis.call('HGET', KEYS[1], 'Id') or '0') ~= tonumber(ARGV[2])
or tonumber(redis.call('HGET', KEYS[1], 'CacheSchema') or '0') ~= tonumber(ARGV[3])
or redis.call('HEXISTS', KEYS[1], 'Quota') == 0 then
return -1
end
redis.call('HINCRBY', KEYS[1], 'Quota', tonumber(ARGV[1]))
return 1`
func quotaResultFromLua(result int, err error) (cacheQuotaResult, error) {
if err != nil {
return cacheQuotaMiss, err
}
switch result {
case 1:
return cacheQuotaOK, nil
case 0:
return cacheQuotaInsufficient, nil
default:
return cacheQuotaMiss, nil
}
}
func cacheTryReserveUserQuota(userID int, amount int64) (cacheQuotaResult, error) {
result, err := common.RDB.Eval(context.Background(), userQuotaReserveScript,
[]string{getUserCacheKey(userID)}, amount, userID, userCacheSchemaVersion).Int()
return quotaResultFromLua(result, err)
}
func cacheApplyUserQuotaDelta(userID int, delta int64) (cacheQuotaResult, error) {
result, err := common.RDB.Eval(context.Background(), userQuotaDeltaScript,
[]string{getUserCacheKey(userID)}, delta, userID, userCacheSchemaVersion).Int()
return quotaResultFromLua(result, err)
}