摘要
在现代互联网应用中,消息推送是用户触达的核心手段。从站内信、短信、邮件到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);
}
}