
本文探讨了如何在Node.js应用中高效管理具有全局并发限制和资源独占时长的特定资源(如“标题”)访问。针对传统队列方案在多资源类型并发场景下的局限性,提出并详细阐述了基于Redis的“火速请求,轮询获取”策略。该方案通过分离请求处理与资源分配,利用Redis的原子操作和过期机制实现资源锁与用户排队,有效解决了长连接阻塞和并发等待问题,提升了系统可伸缩性和用户体验。
在Node.js应用中,我们经常面临需要管理有限资源访问的场景。例如,一个系统允许用户请求特定“标题”(如标题A或标题B),但为了维护系统稳定性或业务逻辑,可能存在以下限制:
传统的基于内存队列(如p-queue)的解决方案,通过简单地嵌套队列来尝试实现这种复杂逻辑时,往往会遇到非预期的阻塞问题。例如,在上述场景中,用户3可能会错误地被阻塞,直到用户1的标题A独占期结束,这显然违背了类别隔离的初衷,并导致用户体验下降。
这种问题的核心在于,将长时间的资源独占(60秒)与初始的资源获取逻辑耦合在同一个处理流程中,并且试图通过单进程内存队列来管理分布式或长时间的资源状态。为了解决这一挑战,我们需要一种更健壮、可伸缩的方案,将资源状态管理从核心业务处理中分离出来。
为了克服上述挑战,我们引入了基于Redis的“火速请求,轮询获取”(Fire-and-Forget with Polling)策略。
为什么不直接持有HTTP连接? 直接在HTTP请求处理过程中持有连接60秒,直到资源释放,存在以下显著问题:
“火速请求,轮询获取”模式 这种模式将用户请求分为两个阶段:
Redis的关键作用 Redis作为高性能的内存数据库,在此模式中扮演了至关重要的角色:
本方案的核心在于利用Redis存储和管理标题的独占状态和用户等待队列。
资源持有者键 (String):
MyBB的全称是mybboard,是一个基于PHP+MySQL搭建,功能强大,高效的开源论坛系统。 MyBB 使用了标准的论坛结构和模式,所以您的用户可以在您的论坛获得良好的用户体验。用户可以通过用户控制面板来自定义他们访问论坛的方式或者自定义他们想看到的论坛的内容,他们还可以方便地发表和答复一个主题并且标记与他们有关的主题。论坛管理员和版主可以使用MyBB的内置编辑器和版主工具等功能,控制并维
95
标题等待队列 (List):
以下是基于Node.js和ioredis库的实现示例。
当用户请求某个标题时,服务器将用户ID添加到对应标题的Redis等待队列中,并立即返回一个确认信息给客户端。
// 假设使用ioredis客户端
import Redis from 'ioredis';
import express from 'express';
const redis = new Redis();
const app = express();
app.use(express.json()); // 用于解析请求体
/**
* 处理用户请求标题的端点
* @param {string} req.body.userId - 用户唯一ID
* @param {string} req.body.titleId - 请求的标题ID (例如 'A', 'B')
*/
app.post('/request-title', async (req, res) => {
const { userId, titleId } = req.body;
if (!userId || !titleId) {
return res.status(400).json({ message: 'User ID and Title ID are required.' });
}
const queueKey = `title:${titleId}:queue`;
// 将用户ID添加到队列末尾
await redis.rpush(queueKey, userId);
console.log(`User ${userId} requested title ${titleId}, added to queue.`);
res.status(202).json({
message: `Your request for title ${titleId} has been queued.`,
status: 'queued',
// 可以返回一个唯一的请求ID供客户端跟踪
});
});客户端在收到请求入队响应后,开始定期(例如每5秒)向此端点发送请求。服务器逻辑会检查并尝试分配标题。
/**
* 处理客户端轮询检查访问权限的端点
* @param {string} req.query.userId - 用户唯一ID
* @param {string} req.query.titleId - 请求的标题ID
*/
app.get('/check-access', async (req, res) => {
const { userId, titleId } = req.query;
if (!userId || !titleId) {
return res.status(400).json({ message: 'User ID and Title ID are required.' });
}
const holderKey = `title:${titleId}:current_holder`;
const queueKey = `title:${titleId}:queue`;
const TITLE_HOLD_SECONDS = 60; // 标题独占时长
// 1. 检查当前用户是否已持有标题
const currentHolder = await redis.get(holderKey);
if (currentHolder === userId) {
console.log(`User ${userId} already holds title ${titleId}.`);
return res.json({ status: 'granted', titleId });
}
// 2. 如果标题未被持有或已过期,尝试获取
if (!currentHolder) {
// 检查队列中第一个用户是否是当前用户
const [firstInQueue] = await redis.lrange(queueKey, 0, 0);
if (firstInQueue === userId) {
// 尝试原子性地设置持有者,并设置过期时间
// 'NX': Only set the key if it does not already exist.
// 'EX': Set the specified expire time, in seconds.
const acquired = await redis.set(holderKey, userId, 'EX', TITLE_HOLD_SECONDS, 'NX');
if (acquired) {
// 成功获取,从队列中移除该用户
await redis.lrem(queueKey, 1, userId);
console.log(`User ${userId} successfully acquired title ${titleId}.`);
return res.json({ status: 'granted', titleId });
}
// 如果acquired为null,说明在尝试SET NX EX时,有其他进程/线程抢先设置了,
// 此时当前用户需要继续等待下一次轮询
}
}
// 3. 标题被其他用户持有,或当前用户不在队列首位,继续等待
console.log(`User ${userId} is waiting for title ${titleId}. Current holder: ${currentHolder || 'None'}`);
res.json({ status: 'waiting', titleId, currentHolder: currentHolder || null });
});用户选择提前释放标题时,客户端发送请求。服务器删除current_holder键。
/**
* 处理用户手动释放标题的端点
* @param {string} req.body.userId - 用户唯一ID
* @param {string} req.body.titleId - 释放的标题ID
*/
app.post('/abort-title', async (req, res) => {
const { userId, titleId } = req.body;
if (!userId || !titleId) {
return res.status(400).json({ message: 'User ID and Title ID are required.' });
}
const holderKey = `title:${titleId}:current_holder`;以上就是Node.js中利用Redis构建高效的并发资源访问与排队系统的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号