1. 消息通知系统的核心挑战与架构选型
现代互联网应用中,消息通知系统承担着用户活跃度维系和实时交互体验的关键角色。我曾主导过多个日活千万级产品的通知系统重构,发现传统轮询方案在性能和实时性上存在明显瓶颈。以电商订单状态通知为例,采用HTTP短轮询时,服务器在高峰期要处理超过60%的无用请求,而改用WebSocket后带宽成本直接下降83%。
这套架构选择WebSocket作为通信基石主要基于三个考量:首先,它提供了真正的全双工通信能力,服务端可以随时主动推送;其次,相比SSE(Server-Sent Events),它能同时支持双向数据流;最后,现代浏览器对WebSocket协议的支持率已达98.7%。但要注意的是,WebSocket连接本身是有状态的,这意味着我们需要在网关层做连接管理。
消息队列选用Kafka而非RabbitMQ,主要看中其分区存储和消费组的设计。当需要广播一条全站公告时,Kafka的单个主题可以被多个消费者组同时读取,而订单状态变更这类业务消息则可以通过分区键确保顺序处理。实测数据显示,在消息积压量达到500万条时,Kafka的P99延迟仍能保持在200ms以内。
Redis在这里扮演着三重角色:作为WebSocket连接的路由表存储(使用Hash结构),作为未读消息的暂存区(SortedSet实现优先级队列),以及热点数据的缓存层。我们采用CRC16分片算法将不同用户的连接信息分布到多个Redis节点,避免单个热点Key造成性能瓶颈。
TiDB的引入解决了传统方案中MySQL分库分表带来的运维痛点。通知的已读状态这类需要强一致性的数据,通过TiDB的分布式事务保证可靠性。在最近一次大促中,我们的TiDB集群平稳处理了每秒12万次的已读状态更新请求,且P95响应时间稳定在15ms以下。
关键设计原则:WebSocket管实时,Kafka管削峰,Redis管状态,TiDB管持久化。四者通过协同分工实现高并发下的稳定服务。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WebSocket连接管理与心跳保活机制
实现一个工业级的WebSocket服务绝非简单的@ServerEndpoint注解就能搞定。我们的生产环境使用Netty构建WebSocket服务层,核心在于解决三个问题:连接维护、协议升级和流量控制。
连接注册环节采用两级存储策略:当用户Alice连接时,网关首先在本地JVM的ConcurrentHashMap中记录连接Channel,同时向Redis写入路由信息(结构为:notif:route:{userId} -> gateway1)。这种设计使得系统在横向扩展时,能快速定位连接所在的网关实例。我们曾因漏掉这一步,导致集群环境下30%的消息被错误路由。
心跳机制是避免僵尸连接的关键。客户端每30秒发送一次ping帧,服务端回应pong。但要注意浏览器对自动重连的实现差异:Chrome在检测到连接异常后会立即重连,而Safari则会采用指数退避策略。我们在客户端实现了智能重连算法,当连续3次心跳超时后,先尝试轻量级的HTTP检查网络状态,确认正常后再重建WebSocket连接。
java复制// Netty中的心跳处理示例
public class WebSocketHeartbeatHandler extends IdleStateHandler {
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) {
if (evt.state() == IdleState.READER_IDLE) {
ctx.close(); // 关闭超时连接
redis.del("notif:route:" + userId); // 清理路由表
}
}
}
连接中断时的补偿策略值得特别关注。移动端网络切换时平均会有1.2秒的连接闪断,我们通过在服务端设置15秒的grace period来避免频繁重连造成的资源浪费。当监测到iOS设备时,这个阈值会调整到25秒以适配其后台连接管理机制。
压测数据显示,单台8核16G的网关节点可以维持12万条稳定连接。关键在于优化WebSocket帧的编码方式:我们对文本消息采用Thrift二进制编码后传输,体积比JSON减少42%,解析速度提升3倍。
3. Kafka消息分区与消费组设计
消息分区策略直接影响系统的水平扩展能力。我们的Kafka主题按业务线划分为notif.order、notif.social等,每个主题再根据用户ID的哈希值进行分区。这种设计确保同一用户的消息总是由同一个消费者处理,避免了订单状态变更通知乱序的问题。
消费者组的部署需要权衡延迟和资源消耗。对于即时聊天消息,我们部署了独占消费者组保证低延迟;而对于营销类通知,则采用共享消费者组模式,通过调整fetch.min.bytes参数来降低服务器负载。一个常被忽视的参数是session.timeout.ms,我们将其设置为25秒(默认值的2倍),有效减少了因GC停顿导致的消费者假死现象。
消息格式的设计经历了三次迭代。最初采用纯JSON,后来升级为带Schema的Avro格式,最终定型为自定义二进制协议。一条完整的通知消息包含:
code复制[消息版本][事件时间][TTL][业务类型][优先级][发送者][接收者][内容体]
这种结构使得消费者可以快速过滤掉过期消息(通过TTL字段),而不必完整反序列化内容体。
消息积压时的应急方案至关重要。我们开发了分级降级策略:当积压超过10万条时,自动跳过低优先级消息的推送;超过50万条时,改用摘要压缩推送(如"您有5条新消息")。这套机制在去年双十一期间成功将系统从雪崩边缘拉回,当时Kafka积压量一度达到120万条。
避坑指南:永远不要在Kafka消费者中执行阻塞操作。我们曾因在消息处理中同步调用第三方支付接口,导致消费者线程池耗尽,整个系统停滞。
4. 未读消息的存储与补发策略
未读消息管理面临的核心矛盾是:实时查询压力与存储成本的平衡。我们的方案采用三级存储体系:
- 热存储:Redis SortedSet存储最近72小时的未读消息,按时间戳排序
bash复制ZADD notif:unread:{userId} 1625097600 "msgId1" - 温存储:TiDB的行存表存放30天内的消息
- 冷存储:对象存储归档历史消息
补发逻辑的触发条件有四种:用户主动刷新页面、心跳包携带的版本号不一致、服务端检测到连接中断、定时任务扫描长期未活跃用户。其中最难处理的是跨设备场景——当用户在手机端阅读消息后,需要实时同步到PC端。我们通过Redis的PUBSUB机制广播已读事件,配合版本号校验实现最终一致性。
消息去重是另一个技术难点。每条消息生成时都会携带唯一的fingerprint,计算规则为:
code复制fingerprint = MD5(业务类型 + 发送者 + 接收者 + 内容摘要)
在补发环节通过Redis的SETNX操作实现原子性判重,避免用户收到重复通知。这套机制将我们的重复通知率从0.7%降到了0.02%以下。
分页查询优化采用了游标缓存技术。首次查询时在TiDB执行:
sql复制SELECT * FROM notifications
WHERE user_id = ? AND status = 'unread'
ORDER BY create_time DESC LIMIT 20
同时将结果集的第一页和最后一条消息的时间戳缓存到Redis。后续查询若时间范围在缓存窗口内,直接走Redis获取,减少数据库压力。实测显示这种方案使90%的查询响应时间从120ms降至15ms。
5. 已读回写的分布式事务处理
已读状态更新看似简单,实则暗藏玄机。我们遇到过MySQL主从延迟导致已读状态回显不一致的问题,最终通过TiDB的乐观事务解决了这一痛点。核心流程如下:
- 客户端发送已读确认,携带消息ID列表和当前设备标识
- 网关层合并相同用户的消息请求,批量提交
- TiDB执行乐观事务:
sql复制BEGIN OPTIMISTIC; UPDATE notification_records SET status = 'read', read_time = NOW() WHERE message_id IN (?,?,?) AND user_id = ?; COMMIT; - 通过Redis PUBSUB广播已读事件到其他在线设备
对于海量消息的已读操作(如"全部标记为已读"),我们采用了异步批处理模式。服务端先快速响应客户端,后台线程再分批次更新数据库。这里有个精妙的设计:在Redis记录操作流水号,客户端轮询查询处理进度,避免前端显示状态不一致。
事务冲突的解决方案经历了三次演进。最初采用重试机制,但在高并发下效果不佳;后来引入二级提交表,将冲突检测前置;最终方案是利用TiDB的Async Commit特性,将平均处理耗时从230ms降至90ms。关键配置项包括:
toml复制[txn]
async-commit = true
one-pc = true
数据一致性校验通过定时对账任务保证。每天凌晨扫描TiDB与Redis的状态差异,对异常数据进行补偿修复。这套机制帮助我们发现了多个隐蔽的边界条件问题,比如Kafka消费者重启导致的小概率消息重复处理。
6. 性能调优与异常监控体系
全链路压测是系统上线的必经之路。我们开发了模拟用户行为工具,可以动态调整在线用户数、消息发送频率和网络抖动参数。关键指标包括:
- WebSocket连接建立成功率(要求>99.98%)
- 消息端到端延迟P99(要求<500ms)
- Kafka消费延迟(要求<1s)
- TiDB事务成功率(要求>99.95%)
监控体系分为四个层级:
- 基础设施层:CPU、内存、网络IO
- 组件层:WebSocket连接数、Kafka积压量、Redis内存碎片率
- 业务层:未读消息量、已写成功率、补发触发次数
- 用户体验层:消息到达率、重复率、点击率
日志收集采用EFK栈,但针对WebSocket做了特殊优化。每条重要事件(连接建立、消息推送、连接断开)都会生成结构化日志,包含设备指纹、网络类型等维度信息。我们曾通过分析日志发现某款国产手机浏览器的WebSocket实现存在bug,会错误关闭正常连接,最终通过UA识别做了特殊兼容。
熔断降级策略基于Hystrix实现,但增加了业务感知能力。例如当检测到大量iOS设备连接异常时,会自动切换备用的长轮询方案;当TiDB响应变慢时,已读回写会降级为异步模式。这些策略使系统在基础设施故障时仍能提供基本服务。
