技术探索与实践实践总结
技术探索与实践总结:一个全栈开发者的真实故事
开篇:一次偶然的技术挑战,打开了我对工程落地的新认知
去年年底,我参与了一个中型电商平台的重构项目。这个项目的初衷是优化老系统在高并发下的性能瓶颈,同时为未来业务增长预留足够扩展空间。作为团队里的主力全栈工程师,我负责前后端架构的整体设计、技术选型以及部分核心模块开发。
项目上线前两个月,我们遇到了一个看起来“小事一桩”的需求——实现用户实时通知推送功能。原本我们计划用前端轮询来拉取消息,但随着讨论深入,我们意识到这会带来不必要的服务器负载和前端体验问题。
于是,我们决定挑战一下自己:引入 WebSocket 实现真正的消息实时推送。
这篇文章想跟你聊聊,我是怎么一步步把这套机制从想法变成线上服务的。过程中踩了不少坑,也学到了不少经验,希望你读完之后能有一些启发或参考价值。
问题描述:不是不能做,而是怎么做才好
最初的想法很简单,客户端通过 WebSocket 长连接订阅自己的频道,当后端有新通知产生时推送给对应客户端即可。
但实际实施起来才发现问题远没有这么简单:
- 长连接管理复杂:大量用户在线带来的内存占用、心跳维护、断线重连如何处理。
- 服务端并发压力大:Node.js 原生 WebSocket 在大规模并发下表现不稳定,容易出现“OOM(Out Of Memory)”。
- 跨节点通信困难:由于我们的后端部署在多台服务器上,如果某个用户的连接落在 A 节点,而需要推送消息的是 B 节点,那如何协调?
- 安全性考虑不足:如何防止非法用户伪造订阅?是否每个用户都需要建立独立通道?
这些问题促使我重新审视整个方案,并开始深入研究各种即时通信技术及其适用场景。

解决方案:从 WebSocket 到集群再到 Redis 的异构集成
经过前期调研和内部讨论,我们最终采用了一套混合方案,结合 WebSocket、Redis Pub/Sub 和微服务架构来满足我们的需求。
架构思路如下图:
+------------------+ +-----------------+
| Client | <---> | Gateway(WS) |
+------------------+ +--------+--------+
|
+-----------------v------------------+
| Message Broker |
| (Redis Pub/Sub) |
+-----------------+------------------+
|
+----------------v-------------------+
| Message Service |
| 处理业务逻辑 & 发送通知事件 |
+-----------------------------------+
简要解释:
- 所有 WebSocket 连接由一个统一的网关(Gateway)进行代理;
- 每个连接对应一个用户 ID 或设备 ID;
- 当某服务产生通知事件时,将消息发布到 Redis;
- 网关监听特定主题的消息,找到匹配连接后主动推送;
- 通过 Nginx 反向代理实现负载均衡,使客户端随机连接到任意网关节点;
- 用户登录鉴权通过 JWT 完成,网关验证 token 合法性后再建立连接。
这种结构的优势在于:
- 可扩展性强:WebSocket 网关可横向扩展,多个节点互不依赖;
- 解耦彻底:业务服务无需关心推送细节,只需发布消息;
- 降低资源消耗:使用 Redis 消息总线减少冗余网络通信。
代码实践:几个关键环节的实现细节
下面是一些关键代码片段和说明,供你参考。
1. 网关服务初始化 WebSocket 与 Redis 监听
// gateway/index.js
const wsServer = new WebSocket.Server({ server });
const redisClient = createRedisClient(); // 创建 Redis 客户端
const userConnections = new Map(); // 存储用户ID和对应的ws连接集合
// WebSocket 连接监听
wsServer.on('connection', (socket, req) => {
const userId = authenticate(req); // 通过请求头解析出用户ID
if (!userId) {
socket.close();
return;
}
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
userConnections.get(userId).add(socket);
socket.on('close', () => {
const conns = userConnections.get(userId);
conns.delete(socket);
if (conns.size === 0) {
userConnections.delete(userId);
}
});
});
// 订阅 Redis 通知主题
redisClient.subscribe('notifications', (err, count) => {
console.log(`Subscribed to ${count} channels`);
});
redisClient.on('message', (channel, message) => {
const parsed = JSON.parse(message);
const { userId, payload } = parsed;
const connections = userConnections.get(userId);
if (connections && connections.size > 0) {
for (const conn of connections) {
if (conn.readyState === WebSocket.OPEN) {
conn.send(JSON.stringify(payload));
}
}
}
});

2. 消息服务发送通知示例
// service/notification-service.js
function sendNotification(userId, content) {
const message = {
userId,
payload: {
type: 'notification',
content,
timestamp: Date.now()
}
};
redisClient.publish('notifications', JSON.stringify(message));
}
3. 心跳机制保证连接有效性
// 每隔30秒检查一次活跃度
setInterval(() => {
userConnections.forEach((sockets, userId) => {
sockets.forEach(socket => {
if (!socket.isAlive) {
socket.terminate(); // 强制关闭
sockets.delete(socket);
} else {
socket.isAlive = false;
socket.send('ping'); // 主动发送心跳
}
});
if (sockets.size === 0) {
userConnections.delete(userId);
}
});
}, 30000);
// 接收 pong 回应
socket.on('message', data => {
if (data.toString() === 'pong') {
socket.isAlive = true;
}
});
踩坑经验:那些深夜里翻过的“错误日志”
说真的,整个开发过程就像一场修行,每解决一个问题就能感受到一些成长。以下是我印象最深的几个“大坑”。
1. 内存泄漏导致网关崩溃
一开始我们使用 Map 来保存用户连接,但没注意在连接关闭后未及时从 Map 中删除。时间一长就出现了内存缓慢上涨的现象,最终引发 Node.js OOM,进程异常退出。
解决方案:给每个连接注册 close 事件清理缓存,配合定时检测机制,确保无残留连接。
2. Redis 消息丢失(当时以为是代码 bug)
有一次上线后发现用户接收不到通知,排查了很久,误以为是网关没监听消息。
结果发现,是因为我们在测试环境中启用了 Redis 的本地版本,而在正式环境中却忘记配置持久化策略,导致部分消息因为队列满被自动丢弃了。
教训:不同环境之间配置差异一定要严格对齐,或者使用自动化工具检查关键配置是否一致。
3. 乱序消息造成用户感知混乱
有一个用户反馈说:“为什么刚收到的通知内容比上次还旧?”后来我们发现是消息广播的时候,某些消息在传输过程中发生延迟,到达客户端的顺序错乱了。
解决方法:在消息体中加入时间戳,客户端根据时间戳排序显示;同时服务端对同一用户的消息按顺序串行处理,避免并行推送冲突。
4. WebSocket 客户端兼容性问题
iOS 上的 Safari 浏览器对 WebSocket 的连接保持不友好,频繁出现断开的情况。后来通过引入 reconnecting-websocket 第三方库解决了大部分问题。
效果总结:不仅实现了需求,更沉淀了一套能力
最终这个推送系统成功支持了超过 50 万用户的同时在线连接,平均推送延迟控制在 50ms 以内,极大提升了用户体验。
更重要的是,我们借此构建了一套通用的实时通信基础设施,在后续直播弹幕、订单状态变更通知、客服聊天等场景中复用,节省了大量重复开发时间。
以下是几个关键指标对比:
| 指标 | 改造前(轮询) | 改造后(WebSocket) |
|---|---|---|
| 服务器 QPS | ~5000 | ~1200(大幅下降) |
| 推送延迟 | 3~5s | < 100ms |
| 客户端响应速度 | 明显卡顿 | 流畅 |
经验分享:写给未来的我,也是写给你
如果你正在考虑搭建一套类似的通知系统,我想送你几个忠告:
不要盲目追求“新技术”,先看清业务场景
WebSocket 并不是解决一切问题的银弹,比如低频推送可以用 SockJS 或 HTTP 流式替代。合理设计消息格式和路由规则
我们的系统初期用的是粗暴的“一对一”订阅模式,后来优化成了基于标签的 Topic 分发,灵活性大大提升。重视监控和日志采集
我们接入了 Prometheus + Grafana,实时看 WebSocket 连接数、消息吞吐量、失败率等指标,这对问题排查帮助巨大。安全永远不能忽略
鉴权建议用 JWT + 白名单校验机制,防止恶意攻击者伪造身份订阅其他用户数据。拥抱云原生和服务网格思维
如果你的服务部署在 Kubernetes 上,可以把网关做成无状态服务,配合 Sidecar 注入来实现连接治理,非常方便。给自己留一手“降级预案”
我们保留了回退到 HTTP Long Polling 的备用通道,关键时刻不至于完全瘫痪。
尾声:技术的本质是解决问题,而不是堆砌花哨的概念
写下这些经历的时候,我不禁回想那个熬夜调 WebSocket 缓冲区大小、调试 Redis Pub/Sub 延迟、盯着日志发呆的夜晚。当时的我其实很焦虑,总觉得“会不会搞砸了?会不会影响项目进度?”
但现在回头看,那次挑战反而让我更加笃定:真正的好技术,是在真实场景中打磨出来的,而不是纸上谈兵。
希望这篇文章能帮你少走弯路,也希望你在自己的开发旅程中,始终保持热爱与耐心。
毕竟,好的系统背后,从来都不只是代码,还有背后的思考、协作和坚持。
共勉!

评论 0