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
相关产品推荐
相关产品推荐

