From b204ed21b782429813c4f57e6940cdbae7dc5818 Mon Sep 17 00:00:00 2001 From: ding113 Date: Thu, 27 Nov 2025 16:21:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=85=E9=9A=9C=E8=BD=AC=E7=A7=BB?= =?UTF-8?q?=E5=90=8E=E6=97=A0=E6=9D=A1=E4=BB=B6=E6=9B=B4=E6=96=B0=20Sessio?= =?UTF-8?q?n=20=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:当 Session 发生故障转移(A 失败→B 成功)后,由于原供应商 熔断器未触发,绑定不会更新到 B,导致后续请求仍优先尝试 A, 造成缓存重复创建和不必要的切换。 修复:新增 isFailoverSuccess 参数,故障转移成功后无条件更新绑定, 减少供应商切换,提高缓存命中率。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app/v1/_lib/proxy/forwarder.ts | 5 ++-- src/lib/session-manager.ts | 39 +++++++++++++++++------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/app/v1/_lib/proxy/forwarder.ts b/src/app/v1/_lib/proxy/forwarder.ts index 610bb02e1..e3eb24ca5 100644 --- a/src/app/v1/_lib/proxy/forwarder.ts +++ b/src/app/v1/_lib/proxy/forwarder.ts @@ -149,12 +149,13 @@ export class ProxyForwarder { // ⭐ 成功后绑定 session 到供应商(智能绑定策略) if (session.sessionId) { - // 使用智能绑定策略(主备模式 + 健康自动回迁) + // 使用智能绑定策略(故障转移优先 + 稳定性优化) const result = await SessionManager.updateSessionBindingSmart( session.sessionId, currentProvider.id, currentProvider.priority || 0, - totalProvidersAttempted === 1 && attemptCount === 1 // isFirstAttempt + totalProvidersAttempted === 1 && attemptCount === 1, // isFirstAttempt + totalProvidersAttempted > 1 // isFailoverSuccess: 切换过供应商 ); if (result.updated) { diff --git a/src/lib/session-manager.ts b/src/lib/session-manager.ts index 20e22fab4..5c490319a 100644 --- a/src/lib/session-manager.ts +++ b/src/lib/session-manager.ts @@ -418,29 +418,16 @@ export class SessionManager { } /** - * 智能更新 Session 绑定(主备模式 + 健康自动回迁) + * 智能更新 Session 绑定 * - * ⚠️ 职责分离:不做分组权限检查(选择器已保证) - * - * 核心策略: - * 1. 首次绑定:直接绑定到成功的供应商(SET NX 避免并发竞争) - * 2. 重试成功:智能决策 - * a) 新供应商优先级更高(数字更小)→ 直接更新(迁移到主供应商) - * b) 新供应商优先级相同或更低 → 检查原供应商健康状态: - * - 原供应商已熔断 → 更新到新供应商(备用供应商接管) - * - 原供应商健康 → 保持原绑定(优先使用主供应商) - * - * @param sessionId - Session ID - * @param newProviderId - 新供应商 ID - * @param newProviderPriority - 新供应商优先级 - * @param isFirstAttempt - 是否首次尝试 - * @returns { updated: 是否更新, reason: 原因说明, details: 详细说明 } + * 策略:首次绑定用 SET NX;故障转移后无条件更新;其他情况按优先级和熔断状态决策 */ static async updateSessionBindingSmart( sessionId: string, newProviderId: number, newProviderPriority: number, - isFirstAttempt: boolean = false + isFirstAttempt: boolean = false, + isFailoverSuccess: boolean = false ): Promise<{ updated: boolean; reason: string; details?: string }> { const redis = getRedisClient(); if (!redis || redis.status !== "ready") { @@ -477,6 +464,24 @@ export class SessionManager { // ========== 情况 2:重试成功(需要智能决策)========== + // 2.0 故障转移成功:无条件更新绑定(减少缓存切换) + if (isFailoverSuccess) { + const key = `session:${sessionId}:provider`; + await redis.setex(key, this.SESSION_TTL, newProviderId.toString()); + + logger.info("SessionManager: Updated binding after failover", { + sessionId, + newProviderId, + newPriority: newProviderPriority, + }); + + return { + updated: true, + reason: "failover_success", + details: `故障转移成功,绑定到供应商 ${newProviderId}`, + }; + } + // 2.1 获取当前绑定的供应商 ID const currentProviderIdStr = await redis.get(`session:${sessionId}:provider`); if (!currentProviderIdStr) {