如何设计支持用户创建实时告警与定时告警的通知系统——百万级规模下的告警调度方案探讨
Hey,针对百万级用户规模下的定时告警调度和实时通知系统设计,我来分享下实际项目里用过的可行方案——轮询数据库确实在量级上来了之后完全扛不住,不管是性能还是实时性都没法满足需求:
一、百万级告警调度的可行方案
1. 分布式时间轮方案
时间轮是处理定时任务的经典思路,单机时间轮在百万级任务下内存压力太大,所以我们通常会用分布式时间轮来适配大规模场景:
- 用Redis的有序集合(ZSet)模拟时间轮:把每个告警的「下次触发时间戳」作为ZSet的
score,告警ID或任务信息作为value。 - 部署多个Worker节点,每个节点定期(比如每秒)从ZSet中取出
score小于等于当前时间的任务,执行触发逻辑。 - 对于周期性告警(比如每天下午3点),触发完成后要计算出下一次的触发时间戳,重新加入ZSet,实现循环调度。
- 优势:Redis的性能足够支撑百万级任务的存储和查询,分布式Worker可以横向扩展,避免单点故障。
2. 基于分布式任务调度框架
如果不想自己造轮子,直接用成熟的分布式任务调度框架是更高效的选择:
- 比如XXL-JOB、Elastic-Job这类框架,自带负载均衡、故障转移、日志监控等功能。
- 核心是把用户自定义的周期规则(比如「每天下午3点」「每周一三五早上7点」)转换成标准的
Cron表达式,然后将任务注册到框架中,由框架负责调度触发。 - 注意:要做好用户规则到Cron的解析适配,比如支持用户设置的「每天」「每周」「每月」等模糊周期,转换成对应的Cron语法。
3. 延迟队列+消息队列方案
对于单次告警或固定周期的告警,延迟队列是个轻量且高效的选择:
- 用RocketMQ的自定义延迟消息,或者RabbitMQ的死信队列实现延迟触发:把告警任务封装成消息,设置对应的延迟时间,消息到期后自动被消费,触发告警。
- 周期性告警的话,在消息被消费触发后,重新生成下一次延迟的消息发送到队列,实现循环。
- 优势:消息队列的吞吐量极高,能轻松支撑百万级任务的并发处理,而且分布式部署天然支持扩展。不过RabbitMQ的延迟级别是固定的,如果用户的自定义周期不匹配固定级别,就不太适用,这时候可以结合时间轮来补充。
二、实时告警通知系统设计
实时通知系统的核心是异步化、高可用、可扩展,具体设计要点如下:
- 异步解耦:告警触发后,不要直接发送通知,而是把通知任务(包含用户ID、告警内容、通知渠道等)发送到消息队列(比如Kafka、RabbitMQ),由专门的通知服务消费处理。这样调度系统不会被通知环节阻塞,能专注于任务调度。
- 多渠道适配:抽象统一的通知接口,比如
INotifyHandler,针对短信、APP推送、邮件、微信等不同渠道实现对应的Handler。发送时根据用户设置的偏好(比如用户只接受APP推送)调用对应的Handler。 - 流量控制与重试:百万级用户下可能出现通知请求突增,需要做限流(比如用Guava RateLimiter或Redis实现分布式限流);同时对发送失败的任务做重试,比如短信失败重试3次,每次间隔递增(1分钟、3分钟、5分钟),并记录重试日志,避免重复通知。
- 用户偏好校验:数据库中存储用户的通知设置(比如静音时间段、是否开启某渠道通知),通知服务发送前必须校验这些设置,比如用户设置了晚上10点到早上8点静音,就跳过发送。
- 幂等性保证:给每个通知任务分配唯一的
notification_id,发送前先通过Redis或数据库检查是否已经发送过,避免同一告警重复通知用户。
内容的提问来源于stack exchange,提问作者pahnman
相关产品推荐
相关产品推荐

