一句话总结
消息推送系统核心:WebSocket 长连接(全双工通信)+ 心跳机制(保活+检测断线)+ ACK 确认(保证消息可靠送达)。架构:接入层(Netty 管理连接)→ 逻辑层(路由、存储)→ 推送层(按用户ID找到连接并推送)。百万连接优化:Netty + epoll(单机可支撑百万连接)、连接路由表(Redis 存储 userID → serverIP 映射)。
初级理解
推送 vs 轮询 vs 长轮询 vs WebSocket
| 方案 | 原理 | 实时性 | 资源消耗 |
|---|---|---|---|
| 短轮询 | 客户端定时请求 | 低 | 高(大量无效请求) |
| 长轮询 | 客户端请求,服务端hold住直到有数据 | 中 | 中 |
| WebSocket | 全双工长连接,双向推送 | 高 | 低(一次握手) |
# WebSocket 握手(HTTP Upgrade)
# 客户端请求
GET /chat HTTP/1.1
Host: server.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
# 服务端响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# 握手成功后,TCP 连接升级为 WebSocket
# 后续通信使用 WebSocket 帧格式(非 HTTP)
一句话总结:WebSocket 全双工长连接,一次握手后双向通信,实时性最高。
中级深入
心跳机制
# 心跳目的
# 1. 保活:防止 NAT/防火墙断开空闲连接
# 2. 检测:及时发现断线,清理无效连接
# 心跳策略
# 客户端:每 30 秒发送 Ping
# 服务端:收到 Ping 回复 Pong,超过 90 秒未收到 Ping 断开连接
# 服务端心跳检测
public class HeartBeatHandler {
# 定时任务:每 30 秒检查所有连接
@Scheduled(fixedRate = 30000)
public void checkHeartBeat() {
long now = System.currentTimeMillis();
for (Connection conn : connections) {
if (now - conn.getLastHeartBeat() > 90000) {
conn.close(); # 90 秒无心跳,断开
}
}
}
}
消息可靠性保证
# 消息可靠性三要素
# 1. ACK 确认:客户端收到消息后回复 ACK
# 2. 重试机制:未收到 ACK 则重试(最多 3 次)
# 3. 去重:客户端根据消息 ID 去重
# 服务端推送流程
# 1. 发送消息(带 msgId)
# 2. 等待 ACK(超时 5 秒)
# 3. 收到 ACK → 标记已送达
# 4. 未收到 ACK → 重试(最多 3 次)
# 5. 3 次重试失败 → 标记失败,走离线消息
# 离线消息
# 用户不在线 → 消息存入 DB
# 用户上线 → 拉取离线消息 → 标记已读
中级要点:心跳保活+检测断线;ACK+重试保证可靠送达;离线消息兜底。
高级拓展
百万连接架构
# 单机瓶颈
# Linux 默认单进程最大文件描述符 1024
# 需要调整:ulimit -n 1000000
# Netty 优化
# 1. 使用 EpollEventLoopGroup(Linux epoll)
# 2. Boss 线程 1 个,Worker 线程 = CPU 核数
# 3. 零拷贝(FileRegion)
# 4. 内存池(PooledByteBufAllocator)
# 集群架构
# 1. 多台接入服务器(Netty)
# 2. Redis 存储路由表:userID → serverIP
# 3. 推送时查路由表 → 转发到对应服务器
# 4. 服务器间通过 MQ 或 RPC 通信
# 路由表
# Redis Hash: user_route
# user:1001 → server:192.168.1.101:8080
# user:1002 → server:192.168.1.102:8080
消息时序性保证
# 问题:分布式环境下消息可能乱序
# 方案:
# 1. 消息带序列号(seqId)
# 2. 客户端按 seqId 排序
# 3. 同一用户的消息路由到同一台服务器(一致性哈希)
# 4. 或使用单线程消费(保证顺序)
实战场景
场景:站内信推送
# 推送流程
# 1. 业务服务发送推送请求
# 2. 推送服务查路由表(userID → serverIP)
# 3. 转发到对应接入服务器
# 4. 接入服务器找到 WebSocket 连接 → 推送
# 推送接口
POST /push
{
"userId": 1001,
"msgId": "msg_001",
"content": "您有一条新消息",
"type": "notification"
}
# 接入服务器处理
public void push(PushMessage msg) {
Connection conn = connectionMap.get(msg.getUserId());
if (conn != null && conn.isActive()) {
conn.send(msg); # 在线 → 直接推送
} else {
offlineStore.save(msg); # 离线 → 存储
}
}
面试模拟
面试官:消息推送系统怎么设计?
你:WebSocket 长连接 + Netty 接入层。心跳保活(30s Ping,90s 超时断开)。消息可靠性:ACK + 重试(3次)+ 离线存储。集群路由:Redis 存 userID → serverIP 映射。百万连接:Netty epoll + 调整文件描述符限制。
面试官:怎么保证消息不丢失?
你:1)ACK 确认机制:客户端收到后回复 ACK,服务端超时重试;2)离线消息:用户不在线时存 DB,上线后拉取;3)消息去重:客户端根据 msgId 去重;4)服务端持久化:消息先落盘再推送。