refactor(redis): 抽出共享 Redis 客户端单例至 shared/lib/redis-client.ts

This commit is contained in:
SpecialX
2026-07-05 17:40:45 +08:00
parent fcf89dfb2c
commit 44f997bad7
2 changed files with 58 additions and 15 deletions

View File

@@ -20,6 +20,7 @@
*/
import { env } from "@/env.mjs"
import { getRedisClient } from "@/shared/lib/redis-client"
import type { RateLimiter, RateLimitParams, RateLimitResult } from "./types"
@@ -77,8 +78,6 @@ interface UpstashRatelimitCtor {
export class RedisRateLimiter implements RateLimiter {
/** 按 `${limit}:${windowMs}` 缓存 Ratelimit 实例,避免重复构造 */
private readonly instances = new Map<string, UpstashRatelimitInstance>()
/** 共享的 Redis 客户端,避免重复连接 */
private redisClient: unknown = null
/** 加载 Ratelimit 构造器的 Promise懒加载 */
private ratelimitCtorPromise: Promise<UpstashRatelimitCtor> | null = null
@@ -91,18 +90,6 @@ export class RedisRateLimiter implements RateLimiter {
}
}
/** 懒加载 @upstash/redis 的 Redis 客户端 */
private async getRedisClient(): Promise<unknown> {
if (this.redisClient) return this.redisClient
// webpackIgnore: 让 @upstash/redis 成为运行时可选依赖,未启用 redis 驱动时不要求安装
const { Redis } = await import(/* webpackIgnore: true */ "@upstash/redis")
this.redisClient = new Redis({
url: env.UPSTASH_REDIS_REST_URL!,
token: env.UPSTASH_REDIS_REST_TOKEN!,
})
return this.redisClient
}
/** 懒加载 @upstash/ratelimit 的 Ratelimit 构造器 */
private async getRatelimitCtor(): Promise<UpstashRatelimitCtor> {
if (this.ratelimitCtorPromise) return this.ratelimitCtorPromise
@@ -124,7 +111,7 @@ export class RedisRateLimiter implements RateLimiter {
const [Ratelimit, redis] = await Promise.all([
this.getRatelimitCtor(),
this.getRedisClient(),
getRedisClient(),
])
const instance = new Ratelimit({

View File

@@ -0,0 +1,56 @@
import "server-only"
import { env } from "@/env.mjs"
/**
* 共享 Redis 客户端单例rate-limit + cache 共用)。
*
* 复用 env.UPSTASH_REDIS_REST_URL / TOKEN。
* 动态 import + webpackIgnore与原 redis-limiter.ts 同模式。
*
* 故障降级:返回 null调用方走 fail-open。
*/
let singleton: unknown | null = null
let loadPromise: Promise<unknown> | null = null
/**
* 获取共享 Redis 客户端。
*
* - 首次调用时动态 import @upstash/redis 并构造 Redis 实例
* - 后续调用复用单例
* - 若 env 未配置 UPSTASH_REDIS_REST_URL/TOKEN返回 null
* - 若动态 import 抛错(包未安装),返回 null
*
* 调用方需自行处理 nullfail-open 降级)。
*/
export async function getRedisClient(): Promise<unknown | null> {
if (singleton) return singleton
if (loadPromise) return loadPromise
if (!env.UPSTASH_REDIS_REST_URL || !env.UPSTASH_REDIS_REST_TOKEN) {
return null
}
loadPromise = (async () => {
try {
// webpackIgnore: 让 @upstash/redis 成为运行时可选依赖
const { Redis } = await import(/* webpackIgnore: true */ "@upstash/redis")
const client = new Redis({
url: env.UPSTASH_REDIS_REST_URL!,
token: env.UPSTASH_REDIS_REST_TOKEN!,
})
singleton = client
return client
} catch (error) {
console.error(
"[redis-client] Failed to load @upstash/redis:",
error instanceof Error ? error.message : String(error),
)
return null
} finally {
loadPromise = null
}
})()
return loadPromise
}