摘要
在现代互联网应用中,消息推送是用户触达的核心手段。从站内信、短信、邮件到APP推送、实时通知,企业需要一个统一的消息中心来管理多种推送渠道。本文基于神话B2C电商平台的实践经验,详细阐述了企业级消息中心的架构设计、技术选型、核心功能实现以及高可用保障方案,重点介绍了多渠道适配、实时推送(WebSocket/SSE)、消息队列集成等关键技术,为构建统一消息平台提供参考。
目录
一、为什么需要消息中心?
1.1 传统推送方式的痛点
在微服务架构流行之前,各业务系统独立实现消息推送功能,这种"烟囱式"架构带来了诸多问题:
问题1:重复建设,资源浪费
订单服务 → 自己实现短信推送
营销服务 → 自己实现短信推送
支付服务 → 自己实现短信推送
...
每个服务都要:
- 对接短信服务商API
- 维护API密钥和配置
- 处理发送失败重试
- 记录发送日志
问题2:多渠道割裂,无法统一管理
同一个业务事件(如订单支付成功),需要推送到多个渠道:
- 站内信通知
- 短信通知
- APP推送
- 邮件账单
但这些渠道分散在不同系统,无法统一配置、统计和监控。
问题3:缺乏灵活性
- 模板硬编码在代码中,修改需要发版
- 无法动态调整推送策略(如渠道降级)
- 难以支持A/B测试
问题4:成本管控困难
- 无法统计各业务的推送量
- 难以设置配额和限流
- 费用分摊不清晰
1.2 消息中心的核心价值
统一消息中心作为基础设施层的核心服务,解决了上述所有问题:
推送渠道
消息中心
业务服务层
订单服务
营销服务
支付服务
用户服务
统一API
渠道管理
模板管理
推送引擎
站内信
短信
邮件
APP推送
WebSocket
SSE
核心优势:
- 统一接入:业务方只需调用一个API
- 多渠道支持:9种渠道(站内信、短信、邮件、APP推送、WebSocket、SSE、企业微信、钉钉、小程序)
- 灵活配置:模板化管理,无需发版
- 成本可控:统一限流、配额管理
- 数据可视:统一统计、监控、报表
1.3 适用场景
消息中心适用于以下场景:
| 场景分类 | 具体场景 | 推荐渠道 |
|---|---|---|
| 交易通知 | 订单确认、支付成功、发货通知 | 站内信 + 短信 + APP推送 |
| 营销活动 | 优惠券发放、限时抢购、会员权益 | 站内信 + APP推送 + 小程序 |
| 系统通知 | 账号安全、服务变更、系统维护 | 站内信 + 邮件 |
| 实时协作 | 在线客服、工单处理、协同编辑 | WebSocket + 站内信 |
| 企业内部 | 审批通知、任务提醒、OA消息 | 企业微信 + 钉钉 |
| 验证码 | 登录验证、支付验证、绑定手机 | 短信 |
二、消息中心核心概念
2.1 消息的生命周期
一条消息从创建到送达,经历完整的生命周期:
创建消息
开始推送
所有渠道成功
部分渠道成功
所有渠道失败
重试失败渠道
重试
超过重试次数
用户查看(站内信)
待发送
发送中
发送成功
部分成功
发送失败
已过期
已读
状态说明:
- 待发送(PENDING):消息已创建,等待推送
- 发送中(SENDING):正在向各渠道推送
- 发送成功(SUCCESS):所有渠道推送成功
- 部分成功(PARTIAL_SUCCESS):部分渠道成功,部分失败
- 发送失败(FAILED):所有渠道推送失败
- 已过期(EXPIRED):超过有效期或重试次数上限
2.2 消息的组成要素
一条完整的消息包含以下要素:
yaml
消息ID: msg_202601071200001
模板编码: ORDER_PAID_SUCCESS
消息类型: TRANSACTIONAL # 交易类/营销类/通知类
业务类型: ORDER # 订单/营销/用户/系统
接收用户: [userId: 10001]
推送渠道: [IN_APP, SMS, PUSH] # 多渠道组合
变量数据:
orderNo: SO202601071200001
amount: 299.00
productName: 商品名称
优先级: HIGH # 高/中/低
过期时间: 2026-01-08 12:00:00
2.3 渠道的分类
消息中心支持9种推送渠道,可分为以下几类:
按实时性分类:
| 实时性 | 渠道 | 延迟 | 适用场景 |
|---|---|---|---|
| 实时 | WebSocket、SSE | < 1秒 | 在线客服、协作编辑 |
| 准实时 | APP推送 | 1-5秒 | 重要通知、营销活动 |
| 异步 | 短信、邮件、站内信 | 5-30秒 | 交易通知、系统通知 |
按触达率分类:
| 触达率 | 渠道 | 说明 |
|---|---|---|
| 高 | 短信 | 99%以上,但成本高 |
| 中 | APP推送、站内信 | 依赖用户在线/打开APP |
| 低 | 邮件 | 可能进垃圾箱 |
按成本分类:
| 成本 | 渠道 | 价格 |
|---|---|---|
| 高 | 短信 | 0.03-0.05元/条 |
| 中 | APP推送 | 按量计费,约0.001元/条 |
| 低 | 站内信、WebSocket、SSE | 仅服务器成本 |
2.4 模板的作用
模板是消息内容的蓝图,实现了内容与代码的解耦:
传统方式(硬编码):
java
// ❌ 修改内容需要发版
String smsContent = "您的订单" + orderNo + "已支付成功,金额" + amount + "元";
模板方式:
java
// ✅ 模板可在管理后台动态修改
模板定义:
短信模板: "您的订单${orderNo}已支付成功,金额${amount}元"
变量定义: [
{name: "orderNo", type: "String", required: true},
{name: "amount", type: "BigDecimal", required: true}
]
代码调用:
messageClient.send("ORDER_PAID_SUCCESS", userId,
Map.of("orderNo", "SO123", "amount", 299.00)
);
模板的优势:
- 内容修改无需发版
- 多渠道内容统一管理
- 支持变量校验和类型转换
- 支持审核流程
三、业界主流方案对比
在设计消息中心之前,我们调研了业界主流的消息推送平台。
3.1 友盟消息推送(第三方SaaS)
定位:移动APP推送服务
核心特性:
- ✅ 专注于APP推送(iOS、Android)
- ✅ 开发者友好,SDK集成简单
- ✅ 支持标签、别名、地理位置推送
- ✅ 提供推送统计和分析
局限性:
- ❌ 仅支持APP推送,不支持站内信、短信等
- ❌ SaaS服务,数据在第三方
- ❌ 无法定制化开发
适用场景:小型APP,无自建需求
3.2 极光推送(第三方SaaS)
定位:移动开发者服务平台
核心特性:
- ✅ APP推送、短信、即时通讯全覆盖
- ✅ 稳定性好,支持千万级并发
- ✅ 提供VIP专线通道(付费)
- ✅ 支持富媒体推送(图片、音频)
局限性:
- ❌ 费用较高(按推送量计费)
- ❌ 数据无法自主管控
- ❌ 无法与企业内部系统深度集成
适用场景:中大型APP,预算充足
3.3 阿里云消息服务(云服务)
定位:企业级消息队列与推送服务
核心特性:
- ✅ 提供消息队列(RocketMQ)+ 移动推送
- ✅ 与阿里云生态深度集成
- ✅ 支持高并发、高可用
- ✅ 按量付费,成本可控
局限性:
- ❌ 需要组合多个云产品
- ❌ 学习成本高
- ❌ 锁定云厂商
适用场景:已采用阿里云的企业
3.4 自建消息中心(自研)
定位:企业内部统一消息平台
核心特性:
- ✅ 完全自主可控
- ✅ 深度定制,满足特殊需求
- ✅ 数据安全,符合合规要求
- ✅ 支持所有渠道(9种)
- ✅ 与企业系统无缝集成
局限性:
- ⚠️ 开发成本高
- ⚠️ 需要专业团队维护
适用场景:大中型企业,有技术团队
3.5 方案对比总结
| 维度 | 友盟/极光 | 阿里云 | 自建(神话) |
|---|---|---|---|
| 渠道数量 | 1-2种 | 2-3种 | 9种 ✅ |
| 数据安全 | 第三方 | 云端 | 自主 ✅ |
| 定制能力 | 弱 | 中 | 强 ✅ |
| 成本 | 高 | 中 | 低(长期) ✅ |
| 技术门槛 | 低 | 中 | 高 |
| 维护成本 | 低 | 低 | 中 |
神话消息中心的选择理由:
- 业务需求:需要支持9种渠道,第三方无法满足
- 数据安全:金融级安全要求,必须自主可控
- 技术积累:有成熟的微服务团队
- 长期成本:自建后边际成本低
四、整体架构设计
4.1 分层架构
神话消息中心采用经典的四层架构:
┌─────────────────────────────────────────────────┐
│ 前端展现层(管理后台) │
│ • 渠道管理 • 模板管理 • 推送记录 • 数据统计 │
└────────────────────┬────────────────────────────┘
│ HTTPS
┌────────────────────┴────────────────────────────┐
│ 接口层(API网关) │
│ • 路由转发 • 鉴权认证 • 限流熔断 • 日志监控 │
└────────────────────┬────────────────────────────┘
│
┌────────────────────┴────────────────────────────┐
│ 业务服务层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 管理服务 │ │ 发送服务 │ │
│ │ • 渠道配置 │ │ • 消息分发 │ │
│ │ • 模板管理 │ │ • 渠道路由 │ │
│ │ • 用户群 │ │ • 发送执行 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 推送服务 │ │ 统计服务 │ │
│ │ • WebSocket │ │ • 实时统计 │ │
│ │ • SSE推送 │ │ • 数据分析 │ │
│ └─────────────┘ └─────────────┘ │
└────────────────────┬────────────────────────────┘
│
┌────────────────────┴────────────────────────────┐
│ 数据存储层 │
│ • MySQL(消息、模板、配置) │
│ • Redis(连接管理、缓存、Pub/Sub) │
│ • RocketMQ(消息队列、异步处理) │
└─────────────────────────────────────────────────┘
各层职责:
- 前端展现层:Vue3 + TypeScript,运营人员可视化管理
- 接口层:Spring Cloud Gateway,统一入口
- 业务服务层:核心业务逻辑,微服务拆分
- 数据存储层:数据持久化和缓存
4.2 核心服务模块
模块划分原则:高内聚、低耦合
| 模块 | 职责 | 技术栈 |
|---|---|---|
| 渠道管理 | 渠道配置、测试、启停 | Spring Boot + MyBatis-Plus |
| 模板管理 | 模板CRUD、审核、版本控制 | Spring Boot + Freemarker |
| 消息发送 | 消息创建、渠道路由、发送执行 | Spring Boot + RocketMQ |
| 推送服务 | WebSocket/SSE连接管理、实时推送 | Spring WebSocket/SSE |
| 用户群管理 | 用户分组、群体推送 | Spring Boot + MySQL |
| 统计分析 | 实时统计、数据分析、报表 | Spring Boot + Redis + ECharts |
4.3 技术选型
后端技术栈
| 技术 | 版本 | 用途 | 选型理由 |
|---|---|---|---|
| Java | 17+ | 开发语言 | 高性能、生态成熟 |
| Spring Boot | 3.3.0 | 微服务框架 | 开发效率高 |
| Spring Cloud | 2023.0.2 | 分布式解决方案 | 全家桶支持 |
| MyBatis-Plus | 3.5.6 | ORM框架 | 简化CRUD |
| MySQL | 8.4.0 | 关系型数据库 | 事务支持、成熟稳定 |
| Redis | Latest | 缓存+Pub/Sub | 高性能、支持多种数据结构 |
| RocketMQ | 5.x | 消息队列 | 支持定时消息、事务消息 |
| Redisson | 3.27.2 | 分布式锁 | 功能强大、易用 |
| MapStruct | Latest | 对象转换 | 编译时生成,性能好 |
前端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue | 3.4+ | 前端框架 |
| TypeScript | 5.x | 类型安全 |
| Element Plus | 2.5+ | UI组件库 |
| ECharts | 5.5+ | 数据可视化 |
| Pinia | 2.1+ | 状态管理 |
4.4 数据库设计
消息中心共有7张核心表:
| 表名 | 说明 | 核心字段 |
|---|---|---|
mc_channel_config |
渠道配置 | channel_type, provider, api_config, rate_limit |
mc_message_template |
消息模板 | template_code, message_type, content_*, variables |
mc_message_record |
消息主记录 | template_code, receiver_id, status, expire_time |
mc_message_send_detail |
发送明细 | record_id, channel_type, receiver, send_status |
mc_user_group |
用户群 | group_name, group_type, member_count |
mc_user_group_member |
用户群成员 | group_id, user_id |
mc_push_record |
推送记录 | group_id, total_count, success_count |
五、多渠道统一管理
5.1 渠道抽象设计
为了支持9种推送渠道并保持可扩展性,我们采用策略模式 + 工厂模式的组合:
核心接口定义:
java
/**
* 渠道提供者接口
* 所有渠道实现类必须实现此接口
*/
public interface ChannelProvider {
/**
* 获取渠道编码
* @return IN_APP, SMS, EMAIL, PUSH等
*/
String getChannelCode();
/**
* 发送消息
*/
ChannelSendResult send(ChannelSendRequest request);
/**
* 测试渠道配置
*/
ChannelTestResult test(ChannelTestRequest request);
}
渠道工厂:
java
@Component
public class ChannelProviderFactory {
// Spring自动注入所有ChannelProvider实现
private final Map<String, ChannelProvider> providerMap;
@Autowired
public ChannelProviderFactory(List<ChannelProvider> providers) {
this.providerMap = providers.stream()
.collect(Collectors.toMap(
ChannelProvider::getChannelCode,
Function.identity()
));
}
public ChannelProvider getProvider(String channelCode) {
ChannelProvider provider = providerMap.get(channelCode);
if (provider == null) {
throw new BusinessException("不支持的渠道: " + channelCode);
}
return provider;
}
}
5.2 支持的9种渠道
| 渠道类型 | 编码 | 服务商 | 使用场景 | 实现类 |
|---|---|---|---|---|
| 站内信 | IN_APP | 自建 | 所有通知 | InAppChannelProvider |
| 短信 | SMS | 阿里云 | 验证码、重要通知 | SmsChannelProvider |
| 邮件 | SMTP/阿里云 | 账单、报表 | EmailChannelProvider | |
| APP推送 | PUSH | 极光/个推 | 实时提醒 | PushChannelProvider |
| WebSocket | WEBSOCKET | 自建 | 在线客服、协作 | WebSocketChannelProvider |
| SSE | SSE | 自建 | Web实时通知 | SseChannelProvider |
| 企业微信 | WECHAT_WORK | 企业微信API | B端内部通知 | WechatWorkChannelProvider |
| 钉钉 | DINGTALK | 钉钉开放平台 | 企业通知 | DingtalkChannelProvider |
| 小程序 | MINI_PROGRAM | 微信开放平台 | 小程序订阅 | MiniProgramChannelProvider |
5.3 渠道配置管理
每个渠道有独立的配置,存储在 mc_channel_config 表:
java
@Data
@TableName("mc_channel_config")
public class ChannelConfig {
private Long id;
// 渠道信息
private String channelType; // 渠道类型(IN_APP/SMS/EMAIL等)
private String channelName; // 渠道名称
private String provider; // 服务商(ALIYUN/JIGUANG等)
// API配置(JSON格式,加密存储)
private String apiConfig; // {"apiKey": "xxx", "apiSecret": "yyy"}
// 限流配置
private Integer rateLimit; // 每小时限制条数
private Integer dailyLimit; // 每日限制条数
// 重试配置
private Integer maxRetry; // 最大重试次数
private Integer retryInterval; // 重试间隔(秒)
// 状态
private Integer status; // 1-启用 0-停用
private Integer priority; // 优先级(用于渠道降级)
}
配置示例(短信渠道):
json
{
"channelType": "SMS",
"channelName": "阿里云短信",
"provider": "ALIYUN",
"apiConfig": {
"accessKeyId": "LTAI5t***",
"accessKeySecret": "nE8K***",
"signName": "神话商城",
"regionId": "cn-hangzhou"
},
"rateLimit": 10000,
"dailyLimit": 100000,
"maxRetry": 3,
"retryInterval": 60,
"status": 1,
"priority": 1
}
5.4 渠道实现示例
短信渠道实现:
java
@Component
public class SmsChannelProvider implements ChannelProvider {
private static final TraceLogger log = TraceLogger.getLogger(SmsChannelProvider.class);
@Autowired
private ChannelConfigRepository channelConfigRepository;
@Override
public String getChannelCode() {
return "SMS";
}
@Override
public ChannelSendResult send(ChannelSendRequest request) {
// 1. 加载渠道配置
ChannelConfig config = channelConfigRepository.findByChannelType("SMS");
if (config == null || config.getStatus() == 0) {
return ChannelSendResult.failed("短信渠道未配置或已停用");
}
// 2. 解析API配置
JSONObject apiConfig = JSON.parseObject(config.getApiConfig());
String accessKeyId = apiConfig.getString("accessKeyId");
String accessKeySecret = apiConfig.getString("accessKeySecret");
String signName = apiConfig.getString("signName");
// 3. 调用阿里云短信API
try {
IAcsClient client = new DefaultAcsClient(
DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret)
);
SendSmsRequest smsRequest = new SendSmsRequest();
smsRequest.setPhoneNumbers(request.getReceiver());
smsRequest.setSignName(signName);
smsRequest.setTemplateCode(request.getTemplateCode());
smsRequest.setTemplateParam(JSON.toJSONString(request.getVariables()));
SendSmsResponse response = client.getAcsResponse(smsRequest);
if ("OK".equals(response.getCode())) {
log.info("短信发送成功, phone={}, bizId={}",
request.getReceiver(), response.getBizId());
return ChannelSendResult.success(response.getBizId());
} else {
log.error("短信发送失败, code={}, message={}",
response.getCode(), response.getMessage());
return ChannelSendResult.failed(response.getMessage());
}
} catch (Exception e) {
log.error("短信发送异常", e);
return ChannelSendResult.failed("发送异常: " + e.getMessage());
}
}
@Override
public ChannelTestResult test(ChannelTestRequest request) {
// 测试短信配置是否正确
try {
JSONObject apiConfig = JSON.parseObject(request.getApiConfig());
// 发送测试短信
ChannelSendRequest testRequest = new ChannelSendRequest();
testRequest.setReceiver(request.getTestReceiver());
testRequest.setContent("【测试】这是一条测试短信");
ChannelSendResult result = send(testRequest);
return result.isSuccess()
? ChannelTestResult.success("测试成功")
: ChannelTestResult.failed("测试失败: " + result.getMessage());
} catch (Exception e) {
return ChannelTestResult.failed("测试异常: " + e.getMessage());
}
}
}
5.5 渠道路由策略
当一条消息需要发送到多个渠道时,消息中心支持多种路由策略:
策略1:全部发送
java
// 所有配置的渠道都发送
List<String> channels = Arrays.asList("IN_APP", "SMS", "PUSH");
策略2:优先级降级
java
// 按优先级尝试,成功一个即停止
// 1. 尝试APP推送(成本低、实时)
// 2. 失败则发送短信(成本高、可靠)
if (!pushChannel.send(request).isSuccess()) {
smsChannel.send(request);
}
策略3:A/B测试
java
// 随机50%用户发送新渠道
if (Math.random() < 0.5) {
newChannel.send(request);
} else {
oldChannel.send(request);
}
5.6 渠道限流保护
为了避免短时间大量消息冲击第三方服务,消息中心实现了多级限流:
限流层级:
通过
拒绝
通过
拒绝
通过
拒绝
消息发送请求
全局限流
渠道限流
返回限流错误
用户限流
发送消息
实现方案(Redis + Lua):
java
@Component
public class RateLimiter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 渠道级限流
* @param channelType 渠道类型
* @param limit 每小时限制数
* @return true-通过 false-拒绝
*/
public boolean tryAcquire(String channelType, int limit) {
String key = "rate:limit:channel:" + channelType;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// 首次访问,设置1小时过期
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
return count <= limit;
}
/**
* 用户级限流(防骚扰)
* @param userId 用户ID
* @param channelType 渠道类型
* @param limit 每天限制数
*/
public boolean tryAcquireUser(Long userId, String channelType, int limit) {
String key = "rate:limit:user:" + userId + ":" + channelType + ":"
+ LocalDate.now();
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.DAYS);
}
return count <= limit;
}
}
六、实时推送技术实现
实时推送是消息中心的核心能力,支持WebSocket和SSE两种模式。
6.1 WebSocket vs SSE
| 维度 | WebSocket | SSE |
|---|---|---|
| 通信方式 | 全双工(双向) | 半双工(服务器→客户端) |
| 协议 | 独立协议(ws://) | HTTP协议 |
| 浏览器支持 | 现代浏览器 | 现代浏览器(除IE) |
| 自动重连 | 需手动实现 | 浏览器自动重连 |
| 传输格式 | 二进制/文本 | 文本(UTF-8) |
| 复杂度 | 中等 | 简单 |
| 适用场景 | 在线客服、游戏 | Web通知、实时数据 |
神话消息中心的选择:
- APP端:使用WebSocket(双向交互需求)
- Web端:使用SSE(单向推送,更简单)
6.2 WebSocket连接管理
连接生命周期:
业务服务 Redis WebSocket服务 客户端 业务服务 Redis WebSocket服务 客户端 loop [心跳保活] 1. 建立连接(携带token) 2. 验证token 3. 注册连接到本地Map 4. 记录连接信息(userId ->> nodeId) 5. 返回连接成功 心跳ping 心跳pong 6. 查询用户连接节点 7. 返回nodeId 8. 推送消息(如果是当前节点) 9. 发送消息 10. 关闭连接 11. 删除连接记录
连接管理器实现:
java
@Component
public class WebSocketConnectionManager {
private static final TraceLogger log =
TraceLogger.getLogger(WebSocketConnectionManager.class);
// 本地连接映射: userId -> WebSocketSession
private final ConcurrentHashMap<Long, WebSocketSession> localConnections =
new ConcurrentHashMap<>();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private String nodeId; // 当前节点ID(从配置读取)
/**
* 注册WebSocket连接
*/
public void register(Long userId, WebSocketSession session) {
// 1. 存储到本地Map
localConnections.put(userId, session);
// 2. 记录到Redis(用于集群路由)
String key = "push:connection:ws:" + userId;
redisTemplate.opsForValue().set(key, nodeId, 2, TimeUnit.HOURS);
log.info("WebSocket连接已注册, userId={}, nodeId={}", userId, nodeId);
}
/**
* 移除连接
*/
public void unregister(Long userId) {
localConnections.remove(userId);
redisTemplate.delete("push:connection:ws:" + userId);
log.info("WebSocket连接已移除, userId={}", userId);
}
/**
* 推送消息给指定用户
*/
public boolean push(Long userId, String message) {
WebSocketSession session = localConnections.get(userId);
if (session == null || !session.isOpen()) {
log.warn("用户未连接或连接已关闭, userId={}", userId);
return false;
}
try {
session.sendMessage(new TextMessage(message));
log.info("消息推送成功, userId={}", userId);
return true;
} catch (IOException e) {
log.error("消息推送失败, userId=" + userId, e);
unregister(userId);
return false;
}
}
/**
* 查询用户连接所在节点
*/
public String findNodeId(Long userId) {
String key = "push:connection:ws:" + userId;
return redisTemplate.opsForValue().get(key);
}
/**
* 获取在线用户数
*/
public int getOnlineCount() {
return localConnections.size();
}
}
WebSocket端点实现:
java
@Component
@ServerEndpoint(value = "/ws/message", configurator = SpringConfigurator.class)
public class MessageWebSocketEndpoint {
private static WebSocketConnectionManager connectionManager;
private static JwtTokenUtil jwtTokenUtil;
@Autowired
public void setConnectionManager(WebSocketConnectionManager manager) {
MessageWebSocketEndpoint.connectionManager = manager;
}
@Autowired
public void setJwtTokenUtil(JwtTokenUtil util) {
MessageWebSocketEndpoint.jwtTokenUtil = util;
}
private Long userId;
private Session session;
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
this.session = session;
// 1. 验证token
try {
this.userId = jwtTokenUtil.getUserIdFromToken(token);
} catch (Exception e) {
log.error("Token验证失败", e);
try {
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "认证失败"));
} catch (IOException ex) {
// ignore
}
return;
}
// 2. 注册连接
connectionManager.register(userId, session);
log.info("WebSocket连接建立, userId={}", userId);
}
@OnClose
public void onClose() {
if (userId != null) {
connectionManager.unregister(userId);
log.info("WebSocket连接关闭, userId={}", userId);
}
}
@OnError
public void onError(Throwable error) {
log.error("WebSocket错误, userId=" + userId, error);
if (userId != null) {
connectionManager.unregister(userId);
}
}
@OnMessage
public void onMessage(String message) {
// 处理客户端消息(如心跳pong)
if ("ping".equals(message)) {
try {
session.getBasicRemote().sendText("pong");
} catch (IOException e) {
log.error("心跳响应失败", e);
}
}
}
}
6.3 SSE推送实现
SSE(Server-Sent Events)是HTML5标准,基于HTTP协议,更简单:
SSE控制器:
java
@RestController
@RequestMapping("/api/message/v1")
public class SseController {
@Autowired
private SseConnectionManager sseConnectionManager;
/**
* 建立SSE连接
*/
@GetMapping("/sse/subscribe")
public SseEmitter subscribe(@RequestParam String token) {
// 1. 验证token
Long userId = jwtTokenUtil.getUserIdFromToken(token);
// 2. 创建SseEmitter(超时2小时)
SseEmitter emitter = new SseEmitter(2 * 60 * 60 * 1000L);
// 3. 注册连接
sseConnectionManager.register(userId, emitter);
// 4. 设置回调
emitter.onCompletion(() -> sseConnectionManager.unregister(userId));
emitter.onTimeout(() -> sseConnectionManager.unregister(userId));
emitter.onError((e) -> sseConnectionManager.unregister(userId));
// 5. 发送欢迎消息
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("连接成功"));
} catch (IOException e) {
log.error("发送欢迎消息失败", e);
}
return emitter;
}
}
SSE连接管理器:
java
@Component
public class SseConnectionManager {
private final ConcurrentHashMap<Long, SseEmitter> localConnections =
new ConcurrentHashMap<>();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private String nodeId;
public void register(Long userId, SseEmitter emitter) {
localConnections.put(userId, emitter);
redisTemplate.opsForValue().set("push:connection:sse:" + userId, nodeId, 2, TimeUnit.HOURS);
}
public void unregister(Long userId) {
localConnections.remove(userId);
redisTemplate.delete("push:connection:sse:" + userId);
}
public boolean push(Long userId, String eventName, Object data) {
SseEmitter emitter = localConnections.get(userId);
if (emitter == null) {
return false;
}
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data));
return true;
} catch (IOException e) {
log.error("SSE推送失败, userId=" + userId, e);
unregister(userId);
return false;
}
}
}
前端SSE接入:
javascript
// 建立SSE连接
const eventSource = new EventSource(`/api/message/v1/sse/subscribe?token=${token}`);
// 监听连接事件
eventSource.addEventListener('connected', (event) => {
console.log('连接成功:', event.data);
});
// 监听消息事件
eventSource.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
console.log('收到消息:', message);
// 显示通知
showNotification(message);
});
// 错误处理
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// 浏览器会自动重连
};
6.4 集群推送方案
在集群环境下,用户连接可能在不同节点,需要跨节点推送:
方案:Redis Pub/Sub
Redis
消息中心集群
业务服务
推送给用户B
查Redis:用户B在节点2
广播消息
推送
订单服务
节点1
用户A连接
节点2
用户B连接
节点3
用户C连接
Pub/Sub
用户B
实现代码:
java
@Component
public class ClusterPushService {
@Autowired
private WebSocketConnectionManager wsManager;
@Autowired
private SseConnectionManager sseManager;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private String nodeId;
/**
* 推送消息给指定用户(支持集群)
*/
public void pushToUser(Long userId, String message, String channelType) {
// 1. 查询用户连接所在节点
String key = "push:connection:" + channelType.toLowerCase() + ":" + userId;
String targetNodeId = redisTemplate.opsForValue().get(key);
if (targetNodeId == null) {
log.warn("用户未在线, userId={}", userId);
return;
}
// 2. 如果在当前节点,直接推送
if (nodeId.equals(targetNodeId)) {
boolean success = "WEBSOCKET".equals(channelType)
? wsManager.push(userId, message)
: sseManager.push(userId, "message", message);
log.info("本地推送{}, userId={}, success={}", channelType, userId, success);
return;
}
// 3. 如果在其他节点,通过Redis Pub/Sub转发
PushMessage pushMsg = new PushMessage(userId, message, channelType, targetNodeId);
redisTemplate.convertAndSend("channel:push", JSON.toJSONString(pushMsg));
log.info("跨节点推送{}, userId={}, targetNode={}", channelType, userId, targetNodeId);
}
}
/**
* Redis消息监听器
*/
@Component
public class PushMessageListener implements MessageListener {
@Autowired
private WebSocketConnectionManager wsManager;
@Autowired
private SseConnectionManager sseManager;
@Autowired
private String nodeId;
@Override
public void onMessage(Message message, byte[] pattern) {
String content = new String(message.getBody(), StandardCharsets.UTF_8);
PushMessage pushMsg = JSON.parseObject(content, PushMessage.class);
// 只处理发给当前节点的消息
if (!nodeId.equals(pushMsg.getTargetNodeId())) {
return;
}
// 推送消息
if ("WEBSOCKET".equals(pushMsg.getChannelType())) {
wsManager.push(pushMsg.getUserId(), pushMsg.getMessage());
} else if ("SSE".equals(pushMsg.getChannelType())) {
sseManager.push(pushMsg.getUserId(), "message", pushMsg.getMessage());
}
}
}
七、核心功能详解
7.1 消息发送完整流程
数据库 渠道提供者 渠道路由 RocketMQ 发送服务 消息API 业务服务 数据库 渠道提供者 渠道路由 RocketMQ 发送服务 消息API 业务服务 异步处理 loop [多渠道并发] 1. 发送消息请求 2. 参数验证 3. 加载模板 4. 渲染内容(替换变量) 5. 创建消息记录 6. 发送到消息队列 7. 返回消息ID 8. 返回成功 9. 消费消息 10. 选择渠道 11. 调用渠道发送 12. 记录发送明细 13. 更新消息状态
代码实现:
java
@Service
public class MessageSendServiceImpl implements MessageSendService {
@Autowired
private MessageTemplateService templateService;
@Autowired
private MessageRecordService recordService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送消息
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Long send(MessageSendRequest request) {
// 1. 参数验证
validateRequest(request);
// 2. 加载模板
MessageTemplate template = templateService.findByCode(request.getTemplateCode());
if (template == null) {
throw new BusinessException("模板不存在: " + request.getTemplateCode());
}
// 3. 渲染内容
Map<String, String> contents = renderContents(template, request.getVariables());
// 4. 创建消息记录
MessageRecord record = new MessageRecord();
record.setTemplateCode(request.getTemplateCode());
record.setMessageType(template.getMessageType());
record.setBusinessType(template.getBusinessType());
record.setReceiverId(request.getReceiverId());
record.setChannels(String.join(",", request.getChannels()));
record.setVariables(JSON.toJSONString(request.getVariables()));
record.setStatus(MessageStatus.PENDING.getCode());
record.setPriority(request.getPriority());
record.setExpireTime(calculateExpireTime(request));
recordService.save(record);
// 5. 发送到消息队列
MessageDispatchEvent event = new MessageDispatchEvent();
event.setRecordId(record.getId());
event.setChannels(request.getChannels());
event.setContents(contents);
event.setReceiver(request.getReceiver());
rocketMQTemplate.asyncSend("message-dispatch", event, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息入队成功, recordId={}", record.getId());
}
@Override
public void onException(Throwable e) {
log.error("消息入队失败, recordId=" + record.getId(), e);
// 更新消息状态为失败
recordService.updateStatus(record.getId(), MessageStatus.FAILED);
}
});
return record.getId();
}
/**
* 渲染各渠道内容
*/
private Map<String, String> renderContents(MessageTemplate template,
Map<String, Object> variables) {
Map<String, String> contents = new HashMap<>();
// 站内信内容
if (StringUtils.isNotBlank(template.getContentInApp())) {
contents.put("IN_APP", renderTemplate(template.getContentInApp(), variables));
}
// 短信内容
if (StringUtils.isNotBlank(template.getContentSms())) {
contents.put("SMS", renderTemplate(template.getContentSms(), variables));
}
// 邮件内容
if (StringUtils.isNotBlank(template.getContentEmail())) {
contents.put("EMAIL", renderTemplate(template.getContentEmail(), variables));
}
// APP推送内容
if (StringUtils.isNotBlank(template.getContentPush())) {
contents.put("PUSH", renderTemplate(template.getContentPush(), variables));
}
return contents;
}
/**
* 模板渲染(使用Freemarker或简单替换)
*/
private String renderTemplate(String template, Map<String, Object> variables) {
String result = template;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
String placeholder = "${" + entry.getKey() + "}";
result = result.replace(placeholder, String.valueOf(entry.getValue()));
}
return result;
}
}
7.2 消息重试机制
重试策略:指数退避(Exponential Backoff)
第1次重试: 立即
第2次重试: 1分钟后
第3次重试: 5分钟后
第4次重试: 15分钟后
超过最大次数: 标记为失败
实现代码:
java
@Component
public class MessageRetryScheduler {
@Autowired
private MessageRecordService recordService;
@Autowired
private ChannelDispatcher dispatcher;
/**
* 定时扫描需要重试的消息(每分钟执行)
*/
@Scheduled(cron = "0 * * * * ?")
public void retryFailedMessages() {
// 1. 查询需要重试的消息
LocalDateTime now = LocalDateTime.now();
List<MessageRecord> records = recordService.findRetryable(now);
if (records.isEmpty()) {
return;
}
log.info("开始重试失败消息, count={}", records.size());
// 2. 逐条重试
for (MessageRecord record : records) {
try {
// 增加重试次数
record.setRetryCount(record.getRetryCount() + 1);
// 计算下次重试时间
LocalDateTime nextRetryTime = calculateNextRetryTime(
record.getRetryCount()
);
record.setNextRetryTime(nextRetryTime);
// 执行重试
boolean success = dispatcher.dispatch(record);
if (success) {
record.setStatus(MessageStatus.SUCCESS.getCode());
log.info("消息重试成功, recordId={}", record.getId());
} else if (record.getRetryCount() >= record.getMaxRetry()) {
// 超过最大重试次数
record.setStatus(MessageStatus.FAILED.getCode());
log.warn("消息重试失败,已达最大次数, recordId={}", record.getId());
} else {
// 继续等待下次重试
record.setStatus(MessageStatus.RETRY.getCode());
}
recordService.updateById(record);
} catch (Exception e) {
log.error("消息重试异常, recordId=" + record.getId(), e);
}
}
}
/**
* 计算下次重试时间(指数退避)
*/
private LocalDateTime calculateNextRetryTime(int retryCount) {
int[] delays = {0, 1, 5, 15, 30}; // 分钟
int delayMinutes = delays[Math.min(retryCount, delays.length - 1)];
return LocalDateTime.now().plusMinutes(delayMinutes);
}
}
7.3 群体推送实现
群体推送用于向大量用户发送消息(如活动通知、系统公告):
流程:
小于1000
大于1000
创建群体推送任务
查询用户群成员
成员数量检查
直接推送
分批推送
创建消息记录
每批1000条
发送到消息队列
更新推送统计
实现代码:
java
@Service
public class GroupPushServiceImpl implements GroupPushService {
@Autowired
private UserGroupService userGroupService;
@Autowired
private MessageSendService messageSendService;
@Autowired
private PushRecordService pushRecordService;
/**
* 群体推送
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Long groupPush(GroupPushRequest request) {
// 1. 创建推送记录
PushRecord record = new PushRecord();
record.setGroupId(request.getGroupId());
record.setTemplateCode(request.getTemplateCode());
record.setChannels(String.join(",", request.getChannels()));
record.setVariables(JSON.toJSONString(request.getVariables()));
record.setStatus(PushStatus.PENDING.getCode());
pushRecordService.save(record);
// 2. 查询用户群成员
List<Long> memberIds = userGroupService.getMemberIds(request.getGroupId());
record.setTotalCount(memberIds.size());
pushRecordService.updateById(record);
// 3. 分批推送(异步)
CompletableFuture.runAsync(() -> {
try {
pushToMembers(record.getId(), memberIds, request);
} catch (Exception e) {
log.error("群体推送失败, recordId=" + record.getId(), e);
pushRecordService.updateStatus(record.getId(), PushStatus.FAILED);
}
});
return record.getId();
}
/**
* 分批推送给成员
*/
private void pushToMembers(Long recordId, List<Long> memberIds,
GroupPushRequest request) {
int batchSize = 1000;
int totalBatch = (memberIds.size() + batchSize - 1) / batchSize;
int successCount = 0;
for (int i = 0; i < totalBatch; i++) {
int fromIndex = i * batchSize;
int toIndex = Math.min(fromIndex + batchSize, memberIds.size());
List<Long> batchIds = memberIds.subList(fromIndex, toIndex);
log.info("推送第{}/{}批, size={}", i + 1, totalBatch, batchIds.size());
// 批量发送消息
for (Long userId : batchIds) {
try {
MessageSendRequest sendRequest = new MessageSendRequest();
sendRequest.setTemplateCode(request.getTemplateCode());
sendRequest.setReceiverId(userId);
sendRequest.setChannels(request.getChannels());
sendRequest.setVariables(request.getVariables());
messageSendService.send(sendRequest);
successCount++;
} catch (Exception e) {
log.error("推送失败, userId=" + userId, e);
}
}
// 更新推送进度
pushRecordService.updateProgress(recordId, successCount);
}
// 更新最终状态
pushRecordService.updateStatus(recordId, PushStatus.COMPLETED);
log.info("群体推送完成, recordId={}, total={}, success={}",
recordId, memberIds.size(), successCount);
}
}
八、高可用与性能优化
8.1 高可用架构
集群部署拓扑:
数据层
缓存层
消息队列
应用层(3节点)
接入层
主从复制
主从复制
监控
主从复制
Nginx
负载均衡
IP Hash
消息中心-1
WebSocket/SSE连接
消息中心-2
WebSocket/SSE连接
消息中心-3
WebSocket/SSE连接
RocketMQ-Master
RocketMQ-Slave
Redis-Master
Redis-Slave
Redis-Sentinel
MySQL-Master
MySQL-Slave
各层高可用策略:
- 接入层:Nginx双活 + Keepalived VIP
- 应用层:至少3节点,无状态(连接信息存Redis)
- 消息队列:RocketMQ主从部署
- 缓存层:Redis Sentinel自动故障转移
- 数据层:MySQL主从 + MHA故障切换
8.2 性能优化策略
优化1:消息队列削峰
java
// 高并发场景,先入队再异步处理
@Override
public Long send(MessageSendRequest request) {
// 同步:创建消息记录
MessageRecord record = createRecord(request);
// 异步:发送到消息队列
rocketMQTemplate.asyncSend("message-dispatch", record);
// 立即返回消息ID
return record.getId();
}
效果:
- 接口响应时间从 500ms → 50ms
- 支持 1000+ QPS
优化2:连接预热
java
@Component
public class WebSocketPreWarmer implements ApplicationRunner {
@Autowired
private WebSocketConnectionManager connectionManager;
@Override
public void run(ApplicationArguments args) {
// 启动时预创建连接池
log.info("WebSocket连接池预热开始...");
// 初始化连接管理器资源
connectionManager.preWarm();
log.info("WebSocket连接池预热完成");
}
}
优化3:Redis管道批量操作
java
// 批量查询用户连接节点
public Map<Long, String> batchFindNodeIds(List<Long> userIds) {
List<String> keys = userIds.stream()
.map(id -> "push:connection:ws:" + id)
.collect(Collectors.toList());
// 使用Pipeline批量查询
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String key : keys) {
connection.get(key.getBytes());
}
return null;
});
// 组装结果
Map<Long, String> nodeMap = new HashMap<>();
for (int i = 0; i < userIds.size(); i++) {
if (results.get(i) != null) {
nodeMap.put(userIds.get(i), (String) results.get(i));
}
}
return nodeMap;
}
8.3 监控告警
关键指标:
| 类别 | 指标 | 阈值 | 告警级别 |
|---|---|---|---|
| 在线连接 | WebSocket连接数 | < 100(异常低) | Warning |
| 在线连接 | SSE连接数 | < 50(异常低) | Warning |
| 消息发送 | 消息发送失败率 | > 1% | Critical |
| 消息发送 | 消息发送延迟P99 | > 5秒 | Warning |
| 队列堆积 | RocketMQ堆积消息数 | > 10000 | Critical |
| 系统资源 | Redis内存使用率 | > 80% | Warning |
| 系统资源 | MySQL连接数 | > 200 | Warning |
监控实现(Prometheus + Grafana):
java
@Component
public class MetricsCollector {
@Autowired
private WebSocketConnectionManager wsManager;
@Autowired
private MeterRegistry meterRegistry;
@PostConstruct
public void init() {
// 注册指标:WebSocket在线连接数
Gauge.builder("message.websocket.connections", wsManager,
WebSocketConnectionManager::getOnlineCount)
.description("WebSocket在线连接数")
.register(meterRegistry);
// 注册指标:消息发送成功率
Counter.builder("message.send.success")
.description("消息发送成功次数")
.register(meterRegistry);
Counter.builder("message.send.failed")
.description("消息发送失败次数")
.register(meterRegistry);
}
}