在日常开发与系统运维中,通知噪音(Notification Noise)一直是影响效率的关键痛点。无论是移动端的广告推送,还是企业内部的告警洪流,抑或是客服系统中的重复工单,都指向一个核心需求:精准的通知策略控制。
近期,开源社区的 DoNotNotify 项目进入了我的视野。这款应用并非简单的 “通知栏清理工具”,而是一个架构严谨的本地策略引擎,它利用 Android 的 NotificationListenerService 实现了一套完整的黑名单、白名单、正则匹配以及时间窗口控制逻辑。虽然其定位是移动端应用,但它所展示的规则匹配逻辑、优先级判定与去重机制,恰恰是构建企业级通知治理系统最需要借鉴的底层设计。
本文将以其开源架构为蓝本,深入探讨如何设计一套面向高并发、多租户场景的企业级通知策略引擎,并给出具体的工程化参数与落地建议。
1. 核心架构:从本地监听器到事件流处理
DoNotNotify 的核心在于其实时性与本地化。它不依赖云端服务器进行决策,而是将策略下放至客户端,直接在通知触发的瞬间完成判定。这种架构的优势在于延迟极低(毫秒级)且隐私性极高(无网络权限)。
然而,当我们将其抽象为系统设计时,企业级场景需要的是横向扩展能力与集中化管理。我们需要将这种 “本地决策” 思维转化为 “边缘计算 + 云端大脑” 的混合架构。
1.1 事件摄取层:Agent 与 Sidecar 模式
借鉴 DoNotNotify 在客户端的监听机制,在企业环境中,我们可以采用 Agent/Sidecar 模式。例如,在 Kubernetes 环境中,每个 Pod 旁运行一个轻量级 Agent,负责拦截应用发出的事件(如 Log4j 的 Appender 或业务系统的 Webhook),并将其以极高的吞吐量发送至中心化的消息队列(如 Apache Kafka)。
这一层的设计关键在于非阻塞与低侵入。Agent 不应阻塞业务主线程,而是采用异步发送或本地队列暂存(如内存环缓冲区)的策略。
1.2 策略执行层:规则引擎的选型
DoNotNotify 支持简单的 “包含匹配” 与复杂的 “正则表达式”,这是其规则引擎的核心。企业级系统则需要更强大的表达能力。
规则匹配算法
- 简单匹配(Prefix/Exact/Suffix):适用于海量数据的快速过滤,复杂度为 O (1),通常使用哈希表(Hash Map)实现。
- 正则匹配(Regex):适用于模糊匹配,复杂度取决于正则表达式的复杂度(回溯风险)。DoNotNotify 在移动端实现了此功能,但在高并发服务端,正则匹配通常是性能瓶颈。
- Drools / Ruling Engine:对于复杂业务逻辑(如 “当用户在 A 页面且 IP 为 B 段时,禁止发送营销通知”),引入 Drools 或自研的决策树引擎更为合适。
2. 关键机制:优先级队列与去重策略
通知系统最常见的两个问题是优先级混乱(重要通知被淹没)和重复通知(如服务抖动导致告警风暴)。DoNotNotify 通过简单的 “列表展示” 解决了查看问题,但在企业场景下,我们需要更硬性的工程参数。
2.1 优先级队列设计
企业级通知通常分为多个优先级(P0-P5)。系统内部应维护多个 Topic 或 Partition:
- 高优先级通道(P0-P1):直接推送,允许短信 / 电话 / 弹窗,延迟容忍度 < 100ms。
- 中优先级通道(P2-P3):合并推送(如每日摘要),延迟容忍度 < 1 小时。
- 低优先级通道(P4-P5):批量处理(如周报),可容忍小时级延迟。
在技术实现上,可以使用 Redis ZSet(有序集合)来管理待发送通知,利用分数(Score)作为时间戳或优先级权重,确保 O (log N) 的入队与出队效率。
2.2 去重算法:布隆过滤器与滑动窗口
重复通知是用户体验的杀手。借鉴 DoNotNotify 的本地去重逻辑(基于 App 包名 + Content Text),企业级系统可以采用更复杂的去重策略:
1. 精确去重(Exact Deduplication)
对于关键告警(如 “Server Down”),必须做到绝对精确。使用 Redis Bitmap 或 布谷鸟过滤器(Cuckoo Filter) 存储最近 N 分钟(如 5 分钟)内的通知指纹(Hash)。
关键参数配置建议:
- 去重窗口(Deduplication Window):300 秒(5 分钟)。
- 指纹有效期:滑动过期机制,防止内存溢出。
2. 语义去重(Semantic Deduplication)
对于非关键信息(如 “订单状态更新”),允许一定程度的模糊去重。可以对通知内容进行 Tokenization(如提取关键词 “订单号”、“状态”),仅当核心 Token 重复时才进行拦截,避免因时间戳不同而放过重复消息。
3. 可观测性:不仅仅是记录日志
DoNotNotify 提供了 “通知历史” 与 “被拦截列表”,这在企业系统中对应的是 完整的审计追踪(Audit Trail) 与 策略效果监控。
3.1 指标埋点
必须对以下维度进行监控:
- QPS:每秒处理的策略请求数。
- 命中率:黑名单拦截率、白名单放行率。
- 延迟分布:P50, P90, P99 的策略匹配耗时。
- 错误率:规则解析异常、存储层超时。
3.2 链路追踪
当一条通知被拦截时,系统必须能回答:“它被哪条规则拦截了?” 这要求在日志中附加 rule_id 和 match_condition,并支持通过 TraceID 关联上下游系统。
4. 落地清单:从设计到代码
假设我们需要构建一个微服务 notification-policy-engine,以下是关键的技术选型与参数建议:
| 组件 | 技术选型 | 工程化参数 |
|---|---|---|
| 消息队列 | Apache Kafka | Partition 数量 = 3 * Broker 数量;Retention = 7 天 |
| 规则存储 | PostgreSQL + JSONB | 支持灵活的模式匹配与复杂查询 |
| 缓存层 | Redis Cluster | 主要用于去重(Bitmap)与限流(Token Bucket) |
| 规则引擎 | 自研 DSL 或 Drools | 编译期预热规则,禁止运行时动态加载高复杂度正则 |
| 监控 | Prometheus + Grafana | 告警阈值:Policy Latency P99 > 50ms |
核心伪代码逻辑(Go):
// 简化的策略匹配逻辑
func (s *PolicyService) ProcessNotification(event Notification) Result {
// 1. 语义去重:检查 Redis Bitmap
if s.dedupService.IsDuplicate(event.Fingerprint()) {
return Result{Status: BLOCKED, Reason: "Duplicate"}
}
// 2. 规则匹配:按优先级顺序遍历
for _, rule := range s.getRulesSortedByPriority() {
if rule.Match(event) {
if rule.Action == "BLOCK" {
return Result{Status: BLOCKED, RuleID: rule.ID}
}
}
}
// 3. 放行
return Result{Status: ALLOWED}
}
5. 总结与展望
DoNotNotify 作为一个轻量级开源项目,其价值在于它用最小的权限(无网络)解决了最大的痛点(通知干扰)。它的设计哲学提醒我们:策略执行的权力应该离数据源越近越好,而策略的管理权则应该集中统一。
构建企业级通知治理系统,不仅需要借鉴其规则引擎的灵活性,更需要在此基础上解决规模扩展性、一致性与可观测性三大难题。通过引入消息队列进行削峰填谷,使用布隆过滤器进行高效去重,并结合完善的监控体系,我们才能构建出一个真正可靠的企业级通知中枢。
资料来源:
- DoNotNotify GitHub 仓库:https://github.com/anujja/DoNotNotify
- DoNotNotify 官网开源声明:https://donotnotify.com/opensource.html