Next.js 和 PlanetScale(和 Ably)的实时测验 - 并发问题

问题描述 投票:0回答:1

我正在尝试构建一个实时测验。用户猜测答案,并向我的 Next.js 端点发布帖子(我将转向 Ably 来处理消息的连接/顺序)。

当用户发布答案时,他们会发送以下数据:

  • 回答
  • 游戏ID

端点然后执行以下操作(PlanetScale DB 事务):

  • 查询(游戏表):根据游戏ID获取游戏
  • 查询(猜词表):统计该游戏ID的猜词次数
  • 突变(猜测表):插入新的猜测
  • 查询(userDeadline表):检查用户是否被允许(只能猜测X分钟)
  • Mutation(userDeadline 表):更新(日期)或插入 userDeadline
  • 检查答案是否正确
  • 突变(游戏桌):通过更新状态关闭游戏
  • ...

所有这些步骤都在事务中,这可能太多了。如果我每秒 2 个请求对其进行负载测试,则会立即出错。

交易的原因是猜测表中的猜测次数。游戏桌有一个水平场地。它从 1 开始,如果猜测次数 > level * X,则游戏行级别应更新为 2 并向所有连接的浏览器/用户发送实时事件。游戏只能更新一次到下一关。

我知道潜在的瓶颈和安全/速率限制问题,这里没有详细描述。

处理这种情况的最佳方法是什么?实时发布/订阅系统中一个事务中的多个数据库查询?可能我会有 X 个游戏和 Y 个并发用户,并且系统必须处理相当大的负载。 (+- 1000 个并发用户)

database next.js transactions planetscale ably-realtime
1个回答
0
投票

优化性能和更好地处理并发性的首要任务是简化数据库事务:尝试最大限度地减少事务内完成的工作量,即只包括关键操作:插入猜测和更新用户截止日期。
非关键操作,例如检查和更新游戏级别,是在事务之外执行的。

对于 PlanetScale查询见解 应该会有所帮助。

// Inside POST request handler
await db.transaction(async trx => {
  // Insert guess
  await trx('guessTable').insert({ gameId, answer });
  
  // Update user deadline
  // Assuming userDeadline function updates or inserts based on conditions
  await updateUserDeadline(trx, userId, newDeadline);
});

对于计算猜测值或更新用户截止日期等操作,请考虑使用批处理或异步更新。例如,您可以根据猜测次数定期更新游戏级别,而不是每次猜测都更新游戏级别。例如,可以在交易后异步调用

checkAndUpdateGameLevel
函数(返回 Promise),从而允许 API 无需等待此操作完成即可响应用户。

// Asynchronously check game level and update if necessary
checkAndUpdateGameLevel(gameId).then(() => {
  console.log('Game level checked and updated as needed.');
});

缓存经常读取但不经常更新的数据,例如游戏详细信息和用户权限。这减少了数据库负载并加快了响应时间。确保缓存与数据库的一致性。

// Pseudocode for getting game details with caching
async function getGameDetails(gameId) {
  let gameDetails = cache.get(`gameDetails_${gameId}`);
  if (!gameDetails) {
    gameDetails = await db('gameTable').where({ gameId }).first();
    cache.set(`gameDetails_${gameId}`, gameDetails);
  }
  return gameDetails;
}

另请参阅“使用 PlanetScale Boost 进行 Node.js 查询缓存”以获取更具体的解决方案。

还实施速率限制以防止滥用和排队来管理负载。对用户操作进行排队并异步处理它们可以帮助管理流量峰值。

// Express.js rate limiting example
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

//  apply to all requests
app.use(apiLimiter);

这将避免达到 PlanetScale 自己的速率限制(每分钟 600 个请求,如果达到速率限制,则会出现 429 错误响应代码)。

如果可行,将大型事务拆分为专注于关键部分的较小事务。例如,更新游戏级别可以是与插入猜测不同的单独事务。
POST 处理程序中的事务已经专注于关键更新。异步函数

checkAndUpdateGameLevel
展示了如何拆分逻辑以避免长事务。

// Logic to check if game level needs to be updated
async function checkAndUpdateGameLevel(gameId) {
  // This might include fetching the current guess count,
  // comparing it with the level threshold, and then updating the game level if necessary.
}

最后,确保您的数据库架构针对您正在执行的查询进行了优化。对游戏 ID、用户 ID 和其他频繁查询的字段进行适当的索引可以显着提高性能。

CREATE INDEX idx_guess_game_id ON guessTable(gameId);
CREATE INDEX idx_user_deadline ON userDeadline(userId);

回顾一下:

// Simplified Next.js API handler
export async function postAnswer(req, res) {
  const { answer, gameId } = req.body;
  
  // Begin transaction
  try {
    // Insert guess and update user deadline in transaction
    await db.transaction(async trx => {
      // Insert guess
      await trx('guessTable').insert({ gameId, answer });
      
      // Update user deadline
      // 
    });
    
    // Asynchronously check game level and update if necessary
    checkAndUpdateGameLevel(gameId);
    
    res.status(200).json({ message: 'Guess submitted successfully' });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
}

async function checkAndUpdateGameLevel(gameId) {
  // Logic to check if game level needs to be updated
  // That is done outside of the initial transaction to reduce its duration
}
© www.soinside.com 2019 - 2024. All rights reserved.