企业级消息中心架构设计与实践:多渠道统一推送平台

摘要

在现代互联网应用中,消息推送是用户触达的核心手段。从站内信、短信、邮件到APP推送、实时通知,企业需要一个统一的消息中心来管理多种推送渠道。本文基于神话B2C电商平台的实践经验,详细阐述了企业级消息中心的架构设计、技术选型、核心功能实现以及高可用保障方案,重点介绍了多渠道适配、实时推送(WebSocket/SSE)、消息队列集成等关键技术,为构建统一消息平台提供参考。


目录


一、为什么需要消息中心?

1.1 传统推送方式的痛点

在微服务架构流行之前,各业务系统独立实现消息推送功能,这种"烟囱式"架构带来了诸多问题:

问题1:重复建设,资源浪费

复制代码
订单服务 → 自己实现短信推送
营销服务 → 自己实现短信推送
支付服务 → 自己实现短信推送
...

每个服务都要:

  • 对接短信服务商API
  • 维护API密钥和配置
  • 处理发送失败重试
  • 记录发送日志

问题2:多渠道割裂,无法统一管理

同一个业务事件(如订单支付成功),需要推送到多个渠道:

  • 站内信通知
  • 短信通知
  • APP推送
  • 邮件账单

但这些渠道分散在不同系统,无法统一配置、统计和监控。

问题3:缺乏灵活性

  • 模板硬编码在代码中,修改需要发版
  • 无法动态调整推送策略(如渠道降级)
  • 难以支持A/B测试

问题4:成本管控困难

  • 无法统计各业务的推送量
  • 难以设置配额和限流
  • 费用分摊不清晰

1.2 消息中心的核心价值

统一消息中心作为基础设施层的核心服务,解决了上述所有问题:
推送渠道
消息中心
业务服务层
订单服务
营销服务
支付服务
用户服务
统一API
渠道管理
模板管理
推送引擎
站内信
短信
邮件
APP推送
WebSocket
SSE

核心优势

  1. 统一接入:业务方只需调用一个API
  2. 多渠道支持:9种渠道(站内信、短信、邮件、APP推送、WebSocket、SSE、企业微信、钉钉、小程序)
  3. 灵活配置:模板化管理,无需发版
  4. 成本可控:统一限流、配额管理
  5. 数据可视:统一统计、监控、报表

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种
数据安全 第三方 云端 自主
定制能力
成本 低(长期)
技术门槛
维护成本

神话消息中心的选择理由

  1. 业务需求:需要支持9种渠道,第三方无法满足
  2. 数据安全:金融级安全要求,必须自主可控
  3. 技术积累:有成熟的微服务团队
  4. 长期成本:自建后边际成本低

四、整体架构设计

4.1 分层架构

神话消息中心采用经典的四层架构:

复制代码
┌─────────────────────────────────────────────────┐
│           前端展现层(管理后台)                 │
│  • 渠道管理  • 模板管理  • 推送记录  • 数据统计  │
└────────────────────┬────────────────────────────┘
                     │ HTTPS
┌────────────────────┴────────────────────────────┐
│              接口层(API网关)                   │
│  • 路由转发  • 鉴权认证  • 限流熔断  • 日志监控  │
└────────────────────┬────────────────────────────┘
                     │
┌────────────────────┴────────────────────────────┐
│              业务服务层                          │
│  ┌─────────────┐  ┌─────────────┐              │
│  │ 管理服务     │  │ 发送服务     │              │
│  │ • 渠道配置   │  │ • 消息分发   │              │
│  │ • 模板管理   │  │ • 渠道路由   │              │
│  │ • 用户群     │  │ • 发送执行   │              │
│  └─────────────┘  └─────────────┘              │
│  ┌─────────────┐  ┌─────────────┐              │
│  │ 推送服务     │  │ 统计服务     │              │
│  │ • WebSocket  │  │ • 实时统计   │              │
│  │ • SSE推送    │  │ • 数据分析   │              │
│  └─────────────┘  └─────────────┘              │
└────────────────────┬────────────────────────────┘
                     │
┌────────────────────┴────────────────────────────┐
│              数据存储层                          │
│  • MySQL(消息、模板、配置)                     │
│  • Redis(连接管理、缓存、Pub/Sub)             │
│  • RocketMQ(消息队列、异步处理)               │
└─────────────────────────────────────────────────┘

各层职责

  1. 前端展现层:Vue3 + TypeScript,运营人员可视化管理
  2. 接口层:Spring Cloud Gateway,统一入口
  3. 业务服务层:核心业务逻辑,微服务拆分
  4. 数据存储层:数据持久化和缓存

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
邮件 EMAIL 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

各层高可用策略

  1. 接入层:Nginx双活 + Keepalived VIP
  2. 应用层:至少3节点,无状态(连接信息存Redis)
  3. 消息队列:RocketMQ主从部署
  4. 缓存层:Redis Sentinel自动故障转移
  5. 数据层: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);
    }
}

参考资料

  1. 极光推送官方文档
  2. 阿里云消息服务
  3. RocketMQ官方文档
  4. WebSocket RFC 6455
  5. Server-Sent Events规范
  6. 《深入理解RocketMQ》
相关推荐
tedcloud1232 小时前
taste-skill部署教程:打造个性化AI推荐工作流
服务器·前端·人工智能·系统架构·edge
椰椰椰耶3 小时前
[SpringCloud][14]OpenFeign参数传递方法
后端·spring·spring cloud
Boop_wu6 小时前
[Spring cloud]微服务项目搭建(简易)
spring cloud
ysn111118 小时前
红点框架系统设计
系统架构·c#
我登哥MVP9 小时前
SpringCloud Alibaba 核心组件解析:服务注册与发现(Nacos)
java·spring boot·后端·spring·spring cloud·java-ee·maven
Boop_wu9 小时前
[Spring Cloud] Eureka 配置并使用
spring cloud
番茄去哪了9 小时前
神领物流面试题(一)
java·大数据·中间件
山东点狮信息科技有限公司10 小时前
点狮HRM-HRM系统安全体系与数据保护方案
后端·安全·spring·spring cloud·微服务·系统安全·资产
@insist12310 小时前
系统架构设计师-嵌入式系统核心概念与关键机制
架构·系统架构·软考·系统架构设计师·软件水平考试
我登哥MVP11 小时前
SpringCloud 核心组件解析:分布式配置管理
java·spring boot·分布式·spring·spring cloud·java-ee·maven