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) }