You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TRPC+NextJS接口竞态问题与Discord API限流解决方案咨询

解决TRPC + Next.js中并发请求导致的Discord API限流问题

问题核心

当同一用户同时发起3-4个依赖enforceGuildPermissions中间件的TRPC请求时,所有请求会几乎同时进入getUserGuilds函数。由于第一个请求还未完成Redis缓存写入,后续请求检查缓存后会判定为空,进而重复调用Discord API,最终触发平台限流。人工延时的临时方案既不可靠(延时过短仍会触发问题,过长影响体验),也不优雅。

可行解决方案

1. 分布式锁(Redis锁):保证同一用户请求串行执行

利用Redis的SETNX(仅当键不存在时设置)特性,给每个用户加锁。只有拿到锁的请求才会调用Discord API,其他请求等待锁释放后直接读取缓存,从根源避免重复请求。

修改getUserGuilds函数:

import { redis } from './your-redis-client'; // 替换为你的Redis客户端实例

export const getUserGuilds = async (
  session: Session
): Promise<CachedUserGuild[] | null> => {
  if (!session.user.accessToken || !session.user.id) return null;

  const userId = session.user.id;
  const lockKey = `lock:user-guilds:${userId}`;

  // 先尝试读取缓存
  let webUser = await cache.webUsers.get(userId);
  if (webUser) return webUser.guilds;

  // 获取分布式锁,过期时间设为5秒(防止死锁)
  const lockAcquired = await redis.set(lockKey, '1', {
    nx: true,
    ex: 5,
  });

  if (!lockAcquired) {
    // 未拿到锁,短时间后重试读取缓存
    await new Promise(resolve => setTimeout(resolve, 100));
    webUser = await cache.webUsers.get(userId);
    return webUser?.guilds || null;
  }

  try {
    // 拿到锁后再次检查缓存(避免其他请求已完成写入)
    webUser = await cache.webUsers.get(userId);
    if (webUser) return webUser.guilds;

    // 调用Discord API
    const response = await fetch('https://discord.com/api/users/@me/guilds', {
      headers: { Authorization: `Bearer ${session.user.accessToken}` }
    });
    const guilds = await response.json();
    if (!response.ok || guilds.length <= 0) return null;
    
    // 写入缓存
    await cache.webUsers.create(userId, guilds);

    return guilds;
  } finally {
    // 无论请求成功失败,都释放锁
    await redis.del(lockKey);
  }
};

2. 内存级请求合并:单实例场景下的轻量方案

在服务端维护一个全局Map,记录正在进行的getUserGuilds请求Promise,同一用户的并发请求直接复用已有Promise,避免重复调用API。

注意:该方案仅适用于单实例部署(如单台服务器、非边缘函数环境),多实例部署时每个实例有独立内存,无法共享状态。

首先创建全局请求缓存:

// 全局变量,存储正在进行的用户公会请求Promise
const pendingGuildRequests = new Map<string, Promise<CachedUserGuild[] | null>>();

修改getUserGuilds函数:

export const getUserGuilds = async (
  session: Session
): Promise<CachedUserGuild[] | null> => {
  if (!session.user.accessToken || !session.user.id) return null;

  const userId = session.user.id;

  // 先读取缓存
  const webUser = await cache.webUsers.get(userId);
  if (webUser) return webUser.guilds;

  // 检查是否有正在进行的请求,有则直接复用
  const pendingRequest = pendingGuildRequests.get(userId);
  if (pendingRequest) {
    return pendingRequest;
  }

  // 创建新的请求Promise
  const requestPromise = (async () => {
    try {
      const response = await fetch('https://discord.com/api/users/@me/guilds', {
        headers: { Authorization: `Bearer ${session.user.accessToken}` }
      });
      const guilds = await response.json();
      if (!response.ok || guilds.length <= 0) return null;
      
      await cache.webUsers.create(userId, guilds);
      return guilds;
    } finally {
      // 请求完成后移除缓存记录
      pendingGuildRequests.delete(userId);
    }
  })();

  pendingGuildRequests.set(userId, requestPromise);
  return requestPromise;
};

3. 前端请求合并(辅助方案)

在TRPC客户端层面对同一用户的多个依赖公会权限的请求进行合并,或设置请求队列,避免同时发起大量请求。例如使用@tanstack/react-query的批量查询能力,或手动封装请求合并逻辑。不过该方案仅为辅助手段,无法替代后端的根本解决措施。

方案选择建议

  • 多实例部署(如Vercel边缘函数、集群服务器):优先使用分布式锁方案,可靠性高且适配分布式环境。
  • 单实例部署:可以选择内存级请求合并,实现简单且性能开销小。

内容的提问来源于stack exchange,提问作者kamza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 21:22:57