告警通知扩展到钉钉和短信:多通道路由、Token 缓存与失败隔离

导读

把原有 PC 站内通知扩展到钉钉和短信,表面上只是根据类型多调用两个接口。真正落地时却牵出四个问题:双通道重复查询用户、钉钉 Token 每次发送都重新获取、外部通道失败可能拖慢告警事务,以及“代码接入”很容易被误写成“线上已经打通”。

这篇记录一次多通道通知链路的 review:如何把共享前缀提到路由层、用进程内缓存控制 Token 请求、让各通道互不拖累,并诚实区分钉钉实测成功与短信外部环境仍阻塞的边界。

多通道不是多写两个 if

通知配置支持四种组合:仅 PC、钉钉、短信、钉钉与短信。运行时还要同时考虑总开关、接收人、用户绑定信息以及每个通道的可用性。

如果直接在两个通道方法里各自完成“解析接收人 → 查用户 → 过滤字段 → 发送”,双通道模式就会对同一批用户查询两次。告警量小时不明显,告警风暴下它会放大 RPC、数据库和事务占用。

review 后把执行链路收敛成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart LR
A[告警进入] --> B{通知总开关开启?}
B -->|否| Z[结束]
B -->|是| C[优先推送 PC WebSocket]
C --> D{需要外部通道?}
D -->|否| Z
D -->|是| E[解析接收人]
E --> F[批量查询一次用户]
F --> G{短信?}
F --> H{钉钉?}
G --> I[提取并校验手机号]
H --> J[提取已绑定的钉钉用户 ID]
I --> K[调用短信通道]
J --> L[发送 ActionCard]

PC WebSocket 被放在外部查询之前。即使用户服务、短信平台或钉钉代理异常,浏览器里的基础通知仍然可以先到达。

共享前缀只做一次

重构前,钉钉和短信方法都包含相同的三步:拆分接收人 ID、远程批量查询用户、校验查询结果。双通道模式自然会执行两遍。

重构后,路由层只查询一次,再把同一个用户列表交给通道:

1
2
3
4
5
6
7
8
List<User> users = loadRecipients(receiverUserIds);

if (channels.contains(Channel.SMS)) {
pushSms(users, message);
}
if (channels.contains(Channel.DINGTALK)) {
pushDingTalk(users, message);
}

通道方法只关心自己的数据:短信提取和校验手机号,钉钉过滤未绑定用户并组装卡片。测试用“批量用户查询只调用一次”锁住这个约束,避免未来维护时又把查询塞回各通道。

Token 缓存:小改动,少一半外部请求

最初实现每发一条钉钉消息都会先请求一次 access token,再请求一次异步发送接口。Token 本身有较长有效期,这种写法不仅多一次 HTTP,还可能触发平台限流。

最终使用进程内缓存,并在过期前五分钟主动刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private volatile String cachedToken;
private volatile long expiresAt;

private String getToken() {
long now = System.currentTimeMillis();
if (hasText(cachedToken) && now < expiresAt) {
return cachedToken;
}

synchronized (this) {
now = System.currentTimeMillis();
if (hasText(cachedToken) && now < expiresAt) {
return cachedToken;
}

TokenResponse response = requestToken();
cachedToken = response.token();
expiresAt = now + Math.max(
1_000L,
response.expiresInSeconds() * 1_000L - 5 * 60 * 1_000L
);
return cachedToken;
}
}

这里的双重检查有三个目的:命中缓存时不加锁;并发刷新时只让一个线程请求;提前刷新避免拿到 Token 后刚发送就过期。测试覆盖了“有效期内只请求一次”和“过期后重新请求”。

没有一开始就引入 Redis,是因为 Token 只供单个服务进程使用,本地缓存已经满足当前规模。真正需要多实例共享或精确失效时,再升级集中式缓存更合适。

失败隔离优先于“全部成功”

告警入库是主链路,消息通知是增强能力。钉钉或短信异常不能让告警事务回滚,也不能阻止其他通道继续执行。

最终约束是:

  • PC 通道先执行,不依赖外部用户映射;
  • 钉钉和短信各自捕获异常,单个通道失败不影响另一个;
  • 未绑定钉钉账号、手机号为空或格式无效的用户跳过并记录日志;
  • 手动告警与真实告警复用同一个通知函数,避免出现两套行为。

目前外部调用仍位于告警事务中。异常虽然不会导致回滚,但慢请求仍可能占住数据库连接。这是已确认的技术债,而不是“catch 了异常就没问题”。下一步应在事务提交后异步分发,或者引入可靠事件;本次 review 没有扩大改造范围。

手动触发是最有价值的测试缝

真实告警依赖摄像头、算法和事件上报,拿它做通知联调成本很高。系统已经有手动生成告警的入口,只要它加载完整规则并复用真实的通知方法,就可以在没有真实设备事件时验证下半条链路。

推荐分段验证:

  1. 先直测钉钉发送服务,确认 Token、应用配置和 ActionCard;
  2. 再创建一条只开启单个通道的临时规则;
  3. 用手动告警触发业务服务到发送服务的完整链路;
  4. 最后再用真实设备告警做端到端验收;
  5. 测试完成后清理临时规则。

分段验证能够把“规则没命中”“用户没绑定”“平台凭据错误”“外部网络不通”拆开,不必每次都从真实设备重放整条链路。

测试矩阵与真实验证边界

自动化测试覆盖了这些关键分支:

  • Token 缓存命中与过期刷新;
  • 双通道只查询一次接收人;
  • 加密和明文手机号的兼容处理;
  • 无效手机号和未绑定钉钉用户的过滤;
  • 任一外部通道异常时,PC 通知仍然执行;
  • 手动告警能够加载完整通知配置。

截至本次记录,钉钉发送服务已经真实获取 Token、成功提交 ActionCard,测试人员也收到了卡片并验证了详情跳转。业务告警到钉钉的完整链路仍需要在新版服务部署后做最终验收。

短信则只完成代码侧接入,线上初始化仍被外部数据库账号授权或白名单阻塞。日志中的数据库拒绝访问说明问题位于短信平台环境,不代表通知业务代码已经验证成功,也不应该在发布说明里写成“短信已上线”。

小结

这次 review 最终沉淀了五条可复用经验:

  1. 多通道路由先抽共享前缀,用户和配置只查一次;
  2. 有明确有效期的外部 Token,先用最小的进程内缓存解决重复请求;
  3. 基础通道优先,外部通道彼此隔离,增强能力不能拖垮主链路;
  4. 为复杂链路保留手动触发入口,分段验证比依赖真实设备高效;
  5. “代码已接入”“单通道已实测”“完整链路已上线”是三个不同状态,工程记录必须明确区分。

多通道通知真正难的从来不是多调用两个接口,而是让每个通道在成功时有证据、失败时有边界,并且不把故障扩散回核心业务。