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

基于Redis的Express HTTP长轮询问题:特定作者笔记订阅异常

我来帮你梳理下Express长轮询中作者匹配逻辑的常见问题!这种类似Evernote的订阅场景,最容易踩的坑往往和请求队列的分组管理、匹配条件的一致性有关,咱们一步步拆解排查。

一、先明确长轮询的核心逻辑

长轮询的本质是"挂起请求等待事件触发",对应你的场景要满足两个关键:

  • 按作者维度分组存储挂起的订阅请求,每个/subscribe/:author的请求进来后,加入对应作者的队列等待新笔记。
  • 当新笔记发布时,精准找到对应作者的请求队列,遍历给订阅者返回新笔记,再清空已处理的队列。
二、常见异常原因与修复方案

1. 未按作者正确分组存储请求

如果你的挂起请求存在全局列表里,没有按作者拆分,添加匹配逻辑后要么找不到订阅者,要么给所有用户发错内容。

错误示例:

// 错误:全局单队列,无法区分订阅的作者
let pendingRequests = [];

app.get('/subscribe/:author', (req, res) => {
  pendingRequests.push(res); // 所有请求混在一起
});

app.post('/note', (req, res) => {
  const newNote = new Note(req.body.txt, req.body.author, new Date());
  // 这里根本拿不到每个请求对应的订阅作者,匹配逻辑完全失效
  pendingRequests.forEach(r => {
    // 无法判断该请求是否订阅了当前笔记的作者
    r.json(newNote);
  });
});

修复方案:用对象按作者名作为key存储队列:

// 正确:按作者分组存储挂起请求,结构如 { "maxime": [res1, res2], "alice": [res3] }
const pendingSubscriptions = {};

app.get('/subscribe/:author', (req, res) => {
  const author = req.params.author;
  // 初始化该作者的队列(如果不存在)
  if (!pendingSubscriptions[author]) {
    pendingSubscriptions[author] = [];
  }
  // 将当前请求的res对象加入对应队列
  pendingSubscriptions[author].push(res);

  // 超时处理:避免请求永久挂起,30秒后返回空响应让客户端重新订阅
  setTimeout(() => {
    const queue = pendingSubscriptions[author];
    if (queue) {
      const index = queue.indexOf(res);
      if (index !== -1) {
        queue.splice(index, 1);
        // 队列空了就删除键,节省内存
        if (queue.length === 0) delete pendingSubscriptions[author];
        res.status(204).send();
      }
    }
  }, 30000);
});

2. 作者匹配逻辑的一致性问题

最常见的是大小写不匹配(比如订阅Maxime,但笔记作者是maxime),或者字段拼写错误(比如把req.body.author写成req.body.Author)。

错误示例:

app.post('/note', (req, res) => {
  const { txt, author } = req.body;
  const newNote = new Note(txt, author, new Date());
  // 错误:大小写敏感,可能匹配不到订阅队列
  if (pendingSubscriptions[author]) {
    pendingSubscriptions[author].forEach(r => r.json(newNote));
    pendingSubscriptions[author] = [];
  }
});

修复方案:统一作者名的格式(比如转小写),确保订阅和发布的匹配逻辑一致:

app.post('/note', (req, res) => {
  const { txt, author: noteAuthor } = req.body;
  // 统一转小写,消除大小写差异
  const normalizedAuthor = noteAuthor.toLowerCase();
  const newNote = new Note(txt, normalizedAuthor, new Date());

  // 精准匹配对应作者的订阅队列
  if (pendingSubscriptions[normalizedAuthor]) {
    pendingSubscriptions[normalizedAuthor].forEach(subRes => {
      subRes.json(newNote);
    });
    // 清空队列,避免重复响应
    delete pendingSubscriptions[normalizedAuthor];
  }

  res.status(201).json(newNote);
});

// 订阅接口也同步转小写
app.get('/subscribe/:author', (req, res) => {
  const normalizedAuthor = req.params.author.toLowerCase();
  if (!pendingSubscriptions[normalizedAuthor]) {
    pendingSubscriptions[normalizedAuthor] = [];
  }
  pendingSubscriptions[normalizedAuthor].push(res);
  // 超时处理同上...
});

3. 未处理超时请求导致重复响应

如果挂起的请求超时后没有从队列移除,新笔记发布时会给已经超时的请求发响应,导致客户端收到延迟的错误内容。必须在订阅接口中加入超时清理逻辑(如上面修复方案中的setTimeout)。

三、完整可运行的示例代码
const express = require('express');
const app = express();
app.use(express.json());

// 模拟Note数据结构
class Note {
  constructor(txt, author, date) {
    this.txt = txt;
    this.author = author;
    this.date = date;
  }
}

// 存储所有已发布的笔记
const notes = [];
// 按作者分组的挂起订阅请求队列
const pendingSubscriptions = {};

// 订阅特定作者的新笔记
app.get('/subscribe/:author', (req, res) => {
  const normalizedAuthor = req.params.author.toLowerCase();

  // 优化:订阅时先返回该作者的最新笔记(如果有)
  const latestNote = notes.filter(n => n.author === normalizedAuthor).pop();
  if (latestNote) {
    return res.json(latestNote);
  }

  // 初始化队列
  if (!pendingSubscriptions[normalizedAuthor]) {
    pendingSubscriptions[normalizedAuthor] = [];
  }
  pendingSubscriptions[normalizedAuthor].push(res);

  // 30秒超时清理
  setTimeout(() => {
    const queue = pendingSubscriptions[normalizedAuthor];
    if (queue) {
      const index = queue.indexOf(res);
      if (index !== -1) {
        queue.splice(index, 1);
        if (queue.length === 0) delete pendingSubscriptions[normalizedAuthor];
        res.status(204).send();
      }
    }
  }, 30000);
});

// 发布新笔记
app.post('/note', (req, res) => {
  const { txt, author } = req.body;
  if (!txt || !author) {
    return res.status(400).send('Missing required fields: txt or author');
  }

  const normalizedAuthor = author.toLowerCase();
  const newNote = new Note(txt, normalizedAuthor, new Date());
  notes.push(newNote);

  // 通知所有订阅该作者的用户
  if (pendingSubscriptions[normalizedAuthor]) {
    pendingSubscriptions[normalizedAuthor].forEach(subRes => {
      subRes.json(newNote);
    });
    delete pendingSubscriptions[normalizedAuthor];
  }

  res.status(201).json(newNote);
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 12:05:52