消息推送平台MPSP实战:从消息模型到幂等重试的落地指南
简介MPSPMulti-Protocol Service Processor是一个基于 Java 实现的多协议处理示例项目适合对网络编程、并发处理与服务端架构感兴趣的开发者参考。该源码重点展示 TCP/IP、UDP 等常见协议的接入与解析思路并结合 NIO、线程池、设计模式等关键技术可用于网络服务开发的基础学习或作为项目脚手架。源码包共 23 个文件以 15 个 Java 源文件为核心辅以 Gradle 构建脚本及包装器、properties 配置、Markdown 说明文档和 Windows 批处理脚本整体约 18KB体积小巧便于快速通读。目前已有 443 人浏览学习。通过该项目可观察一个开源 Java 工程的完整目录组织从构建配置、源码结构到测试与版本忽略规则均有涉及有助于理解多协议服务处理器的模块划分与实现方式也能学习 Gradle 构建和项目规范化管理适合需要上手网络编程实战、或想研究多协议服务设计的开发者同时可积累单元测试与配置管理经验。 搞消息推送这件事我在前东家经历过一个特别崩溃的阶段业务线多每个系统自己对接渠道SDK极光、个推、APNs、短信网关各拉各的推送测试环境经常串消息线上出了问题根本查不到这条消息到底发没发出去、从哪个环节断的。后来我牵头把散落的推送逻辑收拢成一套统一平台取名MPSP全称是 Message Push Service Platform。这次想把完整的设计思路、核心细节、踩坑过程都整理出来尤其是消息模型、幂等设计、渠道适配器和重试机制这四块给正在做消息触达体系、或者准备自建推送平台的朋友一个可参考的落地样本。这不是讲PPT式架构是我自己半年多实打实写代码、被线上问题折磨过之后的经验记录。内容适合后端开发、平台架构师以及那些对统一消息中心只有模糊概念、想找一个最小可行方案的团队。1. MPSP整体设计与思路拆解1.1 背景消息触达从能发出去到发得明白早期业务量小的时候每个服务的推送逻辑都是各自写的调用渠道SDK把文案拼好发完就完事。这套方式跑通没问题可一旦业务系统多起来问题就暴露得很彻底。第一是消息不可追踪。用户说没收到验证码你只能去日志里翻看某个微服务有没有打出来一行日志查不动、查不清。第二是渠道被绑死。业务系统直接依赖极光SDK想切换成自建厂商通道得改线上代码风险极高。第三是频率不可控。同一个用户在一天内收到七八条运营推送卸载率飙升这锅还得后端背。第四是重试和幂等完全没有网络抖动一下就丢消息或者发两次。MPSP要干的事情不是重复造一个推送SDK而是把消息接入、内容渲染、投递调度、渠道适配、结果回执、频控防刷这些下沉成平台能力让上游业务只传一个业务语义明确的请求剩下的由平台接管。1.2 平台功能模块划分实际拆分下来整个平台核心是五层模块职责落点接入层对外提供统一HTTP/API协议、消息鉴权接收业务方消息并做参数校验、幂等校验调度层异步解耦、消息分片、延迟投递RocketMQ消息队列、定时任务投递层调用各渠道完成真实下发渠道适配器、通道管理、超时控制管理层模板、频控、黑名单、优先级配置后台配置中心数据层投递记录、回执明细、统计分析MySQL业务库、Redis缓存/计数器这层划分不是一开始就有的。第一版MPSP只有接入层和投递层投递结果直接同步返回后来发现高并发下渠道响应慢整个接口被拖死才把调度层加进去。所以模块设计一定要为真实的流量场景服务而不是为了分层而分层。1.3 技术选型与权衡技术选型上我采用的是团队最顺手、运维成本最低的组合Spring Boot 3.x 写主服务MySQL存业务表和投递记录Redis负责幂等键、频控计数和缓存RocketMQ承担消息异步削峰和延迟消息。渠道端覆盖APNsiOS官方通道、FCM海外Android、国内厂商推送、极光/个推聚合SDK、短信网关以及自建的WebSocket长连接通道。有同事问为什么不用Kafka。主要原因是RocketMQ对事务消息、延迟消息、消息重试这些场景内置支持更好推送平台的核心诉求不是吞吐量而是消息不丢、可重试、可跟踪。Kafka在这块要么自己造轮子要么配置很绕。如果团队已经重度使用Kafka那也不是不行只是你需要额外实现一套延迟队列和死信机制。选型没有标准答案关键是和业务场景匹配。2. 核心细节解析与实操要点2.1 消息模型和幂等键设计MPSP的消息模型我前后重构过三次最终稳定成三个核心实体Message业务消息请求、Task投递任务、Receipt渠道回执。业务方调用接入层接口时提交的是Message平台会为它生成一个或多个Task。比如一条营销消息用户有App和短信两个触达渠道那么Message是1Task就是2每个Task对应一次真实投递动作。基本字段如下trace_id全局跟踪ID建议用雪花算法生成方便日志串联biz_type业务类型比如注册验证码、订单通知、营销活动biz_id业务侧唯一标识用于幂等template_code模板编号内容由平台渲染receiver接收方标识手机号、device_token、推送alias之一channel指定渠道不指定则走路由策略priority优先级影响投递顺序schedule_time定时投递时间空则立即投递expire_time过期时间过了这个时间不再投递幂等键是整个平台最容易翻车的地方。我采用的方案是biz_type biz_id channel三要素做唯一索引同时配合Redis的 SETNX 做接口层幂等拦截。为什么要加channel因为同一个biz_id如果同时推送站内信和短信这两个是不同的Task不能互相去重。如果业务方希望同一条消息多渠道只算一次那biz_id就必须是全局唯一的这需要双方提前约定好。2.2 模板渲染与文案治理文案治理是推送平台容易被低估的一环。没有模板之前业务系统各写各的文案验证码是和验证码为都有用户反馈乱。MPSP做成模板化之后所有推送文案必须在后台配置模板并通过审核业务方提交消息时只传template_code和参数平台统一渲染口径自然收敛。模板引擎我选的是FreeMarker它可以预编译和语法校验。每次模板变更后我会在管理后台触发一次试渲染传入虚拟参数把渲染结果记下来出问题可以快速回滚。渲染失败的消息不会直接丢弃而是进入失败补偿队列这是重要兜底。模板内容占位符推荐统一用${xxx}风格避免同时混用其他模板语法导致维护混乱。2.3 频控与防打扰策略频控是推送平台里最影响用户体验的功能做得太狠会挡住正常消息做得太松等于没做。我的实践是两层限流三个维度的组合拳。第一层是全局开关如果某个真实用户开始投诉消息过多运营会直接从后台把这个用户加入免打扰名单所有非必要消息立即拦截只保留验证码之类的强相关消息。第二层是计数限流用Redis INCR EXPIRE 实现固定窗口针对用户维度维护1小时内最多X条、1天内最多Y条的规则针对单渠道维度再限制单用户单天短信不超过Z条因为短信成本高而且对用户的干扰强度远大于App推送。我还加了一个静默时段配置默认晚上10点到第二天早上8点不允许推送营销类消息只有验证码这类用户主动触发的消息能穿透静默时段。实现上不复杂但效果显著投诉量直线下降。3. 实操过程与核心环节实现3.1 从零搭建一个最小可用的推送API如果你要从零开始先别急着做后台管理、数据分析这些锦上添花的功能一定要先跑通一条最小链路业务方调用接口 - 平台接收入库 - 写入RocketMQ - 消费者读取 - 通过一个虚拟渠道投递 - 回执落库。这一步走通核心骨架就立起来了。接入层核心示例我用Spring Boot写一个ControllerPostMapping(/api/msg/send) public Result send(RequestBody Valid SendMessageReq req) { // 1. 幂等校验Redis DB唯一约束 String idempotentKey req.getBizType() : req.getBizId() : req.getChannel(); Boolean first redisTemplate.opsForValue().setIfAbsent(idempotentKey, 1, Duration.ofHours(24)); if (Boolean.FALSE.equals(first)) { return Result.error(重复消息已拦截); } // 2. 消息入库状态为 INIT MessageEntity message messageService.saveInitMessage(req); // 3. 发送到 MQ触发异步投递 mqProducer.sendMessage(message.getId()); return Result.ok(message.getTraceId()); }这里的核心逻辑是接口必须在1秒内返回消息真正的发送过程放到MQ里去。还有一个容易忽略的点消息入库和发送MQ之间要保持原子性我第一版是先发MQ再落库结果MQ发送成功、数据库挂了消息就永久丢失。后来改成先本地落库再通过RocketMQ的事务消息机制保证两条链路一致。最简单的落地方案是先落库然后发送至MQ如果发送失败就用定时任务扫描INIT状态且超过30秒的消息重新投递到MQ。3.2 渠道适配器的抽象与实现渠道适配器是MPSP里最考验设计能力的部分。每一个渠道对应的SDK、参数格式、回执结构都不一样如果接口设计得不好后面每接一个渠道都要改一大片代码。我定义了一个 Contract 接口所有渠道适配器都必须实现public interface ChannelAdapter { String getChannelCode(); SendResult send(ChannelRequest request); ReceiptParseResult parseCallback(String callbackBody); boolean validateConfig(); }每个渠道一个Adapter实现类通过Spring的依赖注入集合统一注册然后根据channel_code查表路由。这样做的好处是新增渠道只需要新增一个Adapter实现不改变核心流程。接真实渠道时有一个坑非常典型渠道回调接口的时间戳校验非常容易采坑不同的厂商对时间偏差的容忍度不一样有的要求5分钟内有的要求1个小时这个不能统一配置死必须做成渠道级参数。我还建议每个渠道适配器里维护一个通道健康状态每次真实投递如果连续失败达到阈值就把该通道标记为降级路由策略会自动把流量转移到备用通道并发送告警给值班人员。这一套下来渠道故障对业务方基本无感。3.3 幂等投递与重试队列的落地投递状态机是整个平台最核心的部分我最终确定的流转是INIT消息已入库PENDING已进入调度队列SENT已下发给渠道DELIVERED渠道回执已送达CLICKED用户已点击RETRY投递失败等待重试FAILED最终失败DEAD超过最大重试次数进入死信重试策略采用的是指数退避间隔依次为1分钟、5分钟、30分钟、2小时、6小时最多重试5次。超过最大次数进入DEAD状态由后台人工排查或触发补偿任务。重试动作本身也要幂等就是每一条重试记录都有一个唯一的request_id渠道侧依据这个id做去重。投递实现的核心思路是数据库状态先改为SENT然后调用渠道如果调用超时或失败不直接改FAILED而是进入RETRY由独立的重试消费者处理。这里有一个很重要的点渠道回执和本地状态可能会有短暂不一致我采用定时对账任务来兜底每天凌晨扫描所有已下发但超过24小时没有最终回执的任务主动向渠道查询状态并修正。真实环境中发送成功和用户收到是两回事渠道接口返回成功只代表渠道受理了真正可靠的是渠道回调。所以平台在存储层把SENT和DELIVERED分开报表统计也只认DELIVERED。这个认知很重要很多初建推送系统的团队只看send接口返回就认为推送成功数据失真严重。4. 常见问题与排查技巧实录4.1 消息到底丢没丢消息丢失是推送平台最高频的问题。我总结出一个排查习惯沿着trace_id走完消息全生命周期。在接入层、MQ生产、MQ消费、渠道下发、渠道回执这五个节点都打印结构化的日志日志里必须包含trace_id。针对丢失场景关键检查点有三个MQ生产端是否开启了发送确认机制没有确认机制生产端失败是感知不到的。消费端是否是手动ACK如果自动ACK且处理渠道限流抛出异常消息会被误认为消费成功直接丢失。数据库和投递动作是否有一致性保障如果渠道先下发成功、数据库再更新状态失败会出现用户收到了但系统显示未发送的情况。4.2 重复推送怎么根治重复推送普遍发生在渠道超时重试场景。渠道接口超时了本地不知道到底发没发重试就可能导致重复。根治方式就是前面说的幂等设计不仅在接入层做更要在渠道适配器层面实现request_id级别的幂等渠道厂商一般支持这个字段也就是同一request_id多次提交只算一次。如果你对接的渠道不支持幂等那只能做投递前检查或者接受极低概率的重复由业务侧通过biz_id再过滤一次。4.3 渠道限流和推送风暴怎么处理一次大促活动营销消息瞬间涌入渠道QPS被打爆这是真实发生过的场景。处理方案是投递限速给每个渠道配置一个最大QPS超过部分先放进内存队列排队按速率慢慢消费。同时按业务类型隔离通道验证码类消息走高优先级独立队列营销类消息走低优先级队列避免运营消息把系统资源挤占光导致用户在登录时收不到验证码。现象可能原因排查步骤消息完全未下发MQ消费积压/未ACK查看消费者日志检查消费线程数与max.poll设置用户重复收到渠道超时后无幂等核对渠道request_id幂等、DB唯一索引用户收不到验证码被全局频控或静默时段拦截查询频控计数检查消息优先级设置渠道回执与发送不一致渠道异步回调延迟执行对账任务手动向渠道查询状态接口偶尔慢到超时发送动作同步阻塞在请求内检查是否走了异步链路是否缺少削峰队列4.4 刚上线的推送平台先做一次故障演练最后给一个我个人觉得特别重要的经验推送平台上线的第一周一定要安排一次渠道故障演练人为把某个渠道的调用地址改成一个不可达的端口观察消息能不能自动重试、切换备用通道、以及给值班人员发告警。这一步能帮你提前暴露太多问题比如超时时间设置不合理、重试次数过多导致消息积压、降级策略没生效等等。不要等真实故障发生才验证这些逻辑压力测试和故障演练的成本比你在线上救火的成本低一个数量级。MPSP做到现在回头看最大的价值反而不是代码本身而是把整个团队的消息触达从一种模糊的手工劳动变成了一套可追踪、可控制、可复盘的技术能力。消息能不能发出去、发到哪个渠道、用户有没有收到、收到后有没有点这些问题如今都能在五分钟内定位出来。如果你也正准备做类似的平台我的建议是不要一开始就铺大摊子做一堆大数据统计和复杂的规则引擎先把消息模型、幂等、重试、回执对账这几件事钉死跑通一个渠道再慢慢扩展。这一套最小闭环搭完你会对推送这件事的理解比写一万行业务代码更深刻。本文还有配套的精品资源点击获取