统一消息服务
日发送 5000+(邮件约 2000 + 短信约 3000) · 接入内部服务 20+ · 发送延迟秒级
背景与结果
将各业务线自接短信/邮件通道的模式统一收口为「统一接入、统一管控、异步发送」的公司级消息服务。
建设动机:此前会员等服务各自直连短信通道,各服务重复对接、无法统一管控。统一收口后:通道复用、统一配额、统一模板、成本可统计。
初版 2020.10 - 2020.12(3 个月上线),此后长期迭代至 2026.07(管理后台、限流风控等均为迭代产出)。一人独立负责需求收集、架构设计与开发实现(服务端 + 管理后台)。
结果数据:接入内部服务最多时 20+;日发送 5000+(邮件约 2000 封 + 短信约 3000 条,全部服务合计);发送延迟秒级。
技术方案
异步发送模型
业务方携 token 请求发送接口(短信/邮件)→ 过拦截链风控 → 通过则入库为待发送任务并返回此次请求的唯一 ID → 业务方凭该 ID 查询发送结果,或接收回调通知(注册时配置通知地址)。
接口响应与通道解耦:调用方不等待通道执行,通道慢或抖动不影响业务接口的响应时间。
拦截链风控(亮点一)
规则:一天内,N 分钟内只能发送 M 条,超限禁发 K 分钟(N/M/K 按业务方配置;一个业务方可配多条规则,任一命中即禁;短信/邮件独立配置)。多规则语义 = 多级防护:1 分钟 3 条防轰炸 + 1 小时 10 条防骚扰 + 1 天 50 条兜底。
禁发落地:命中规则 → 写 Redis key(TTL = K 分钟)→ 后续请求查 key 即 O(1) 秒判拒绝 → 到期自动解除,零定时清理。
架构:拦截链(Chain of Responsibility)
发送请求 → [token 鉴权] → [app 状态/额度校验] → [频控规则组(多条件任一命中)]
→ [禁发标记检查] → [黑名单] → ... → 入库为待发送任务
↓ 命中规则
写入禁发 key(TTL = K 分钟)
任一节点不通过即拒绝,全部通过才入库——风控前置,被拒请求不占通道额度、不产生无效任务。节点可插拔:新规则只插节点不动已有逻辑;短信链/邮件链复用一套骨架、各自装配节点与配置。
频控计数:Redis ZSet 滑动窗口 + Lua 原子:
- 每个业务方 + 手机号/邮箱维度一个 ZSet:member = 请求唯一 ID(必须唯一,否则集合恒为 1 条、频控失效),score = 时间戳
- 判断规则 =
ZCOUNT [now-N分钟, ∞)与 M 比较——三条规则共用一个 key,加规则零存储变更 - 闭环:
ZADD记账 /ZREMRANGEBYSCORE按最大窗口裁剪 /ZCOUNT数窗口内条数;整体 TTL 兜底,不活跃号码自动清理 - 查禁发 → 裁剪 → 数数 → 记账整套写成一个 Lua 脚本发 Redis,单线程执行天然原子,防并发双双漏过
异步消费与分布式锁(亮点二)
- 服务内本地多线程轮询任务表,每隔 1 秒通过 Redis 抢一次独占锁,抢到才有资格读取待发送任务并执行(发送延迟下限即秒级)
- 锁实现:Redisson 分布式锁,leaseTime 5 秒 + watchdog 自动续期——后台线程定期把锁过期时间续回 5 秒,持锁实例存活就一直持有;实例宕机续期停止,锁最长 5 秒自动释放、其他实例接管。发送耗时长锁不会被抢走,宕机也不会死锁
- 锁粒度:短信、邮件各一把锁,两类发送互不阻塞;量级未达分段锁需求,演进路径敞开(按任务 ID 取模分 N 把锁)
- 单机部署跑分布式架构:锁是 Redis 分布式锁而非 JVM 锁——单机内多线程竞争、多实例部署时跨进程竞争同一把锁,防重复消费语义天然成立,从单机多线程到多机多实例代码零改动
重试与可靠性
- 重试可配置、默认关闭:失败后 1 分钟重发的开关由管理后台控制。实际失败多为号码被运营商风控/无响应(重试无效),通道型失败在云厂商高可用下几乎不发生——重试做成可配置能力而非默认行为,避免无效重试增加发送成本
- 最终失败回调:重试后仍失败的任务,除结果可查询外,失败结果也会回调通知业务方——调用方无需轮询即可感知终态,双通道(查询 + 回调)保证触达
- 任务表按年份归档:超期数据定时迁移归档表(原表名_年份),热表保持当年数据,写入与索引稳定
管理后台(Vue3)
业务方注册、配额与频控规则配置、模板配置、发送记录与统计。