异步发送钉钉消息能有效隔离网络 IO 对主业务线程的阻塞,但若线程池配置不当或缺乏兜底机制,可能导致消息积压甚至丢失。核心在于使用独立线程池、配置合理的拒绝策略,并针对关键告警实现持久化。
先说结论:异步发送能隔离网络 IO 等待,但必须配置独立线程池及拒绝策略,防范消息积压和丢失风险。
- 配置:创建专用线程池,核心线程数建议 2-5,队列长度根据并发设定(如 100)。
- 代码:实现钉钉签名计算,异步任务中捕获异常并记录日志。
- 兜底:关键告警建议持久化到数据库或 Redis,防止应用重启丢失。
- 验证:监控线程池活跃数及主接口 RT 波动。
核心配置与代码实现
直接在业务代码中 new Thread 或使用默认线程池存在资源竞争风险。建议在 Spring Boot 中配置专用的 ThreadPoolTaskExecutor,并在发送工具类中实现安全签名。
1. 异步线程池配置
以下配置示例设置了核心线程数、队列容量及拒绝策略。当队列满时,采用 CallerRunsPolicy 由调用线程执行,防止消息静默丢弃,同时触发主线程降级。
@Bean("dingTalkExecutor")
public ThreadPoolTaskExecutor dingTalkExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:根据并发量设定,一般 2-5 即可
executor.setCorePoolSize(5);
// 最大线程数
executor.setMaxPoolSize(10);
// 队列容量:避免内存溢出,建议 100-200
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("dingtalk-async-");
// 拒绝策略:记录日志并由调用线程执行,避免任务丢失
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}2. 钉钉消息发送工具类
自定义机器人 webhook 需要计算签名。以下代码展示了如何生成 timestamp 和 sign,并通过 RestTemplate 异步发送。
@Component
public class DingTalkSender {
@Autowired
@Qualifier("dingTalkExecutor")
private ThreadPoolTaskExecutor executor;
public void sendAsync(String webhook, String secret, String content) {
executor.execute(() -> {
try {
long timestamp = System.currentTimeMillis();
String sign = generateSign(secret, timestamp);
String url = webhook + "×tamp=" + timestamp + "&sign=" + URLEncoder.encode(sign, "UTF-8");
// 构建消息体
Map msg = new HashMap<>();
msg.put("msgtype", "text");
Map text = new HashMap<>();
text.put("content", content);
msg.put("text", text);
// 发送请求
RestTemplate restTemplate = new RestTemplate();
restTemplate.postForObject(url, msg, String.class);
} catch (Exception e) {
// 必须捕获异常并记录,防止异步线程异常退出
log.error("DingTalk send failed", e);
}
});
}
private String generateSign(String secret, long timestamp) throws Exception {
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
}
} 验证与监控方法
改造完成后,需通过监控指标和日志验证异步效果及消息送达情况。
1. 监控主接口响应时间
对比改造前后核心业务接口的平均响应时间(RT)和 P99 耗时。若异步生效,主接口 RT 不应再出现因等待钉钉 HTTP 响应而产生的毛刺。可通过 Spring Boot Actuator 查看:
curl http://localhost:8080/actuator/metrics/http.server.requests2. 观察线程池状态
监控推送线程池的活跃线程数(ActiveCount)和队列大小(QueueSize)。若队列持续满员,说明发送速度跟不上产生速度,需调整限流策略或扩容线程池。可暴露线程池指标到 Prometheus 或通过 JMX 查看。
3. 检查消息送达日志
在调用 API 时记录详细的日志信息,包括请求时间、响应状态码。确认消息是否成功到达钉钉群,以及是否有因限流(错误码 310001)或签名错误(错误码 310002)导致的失败记录。
常见风险与规避
1. 线程池拒绝策略缺失
若未配置拒绝策略,默认策略可能直接丢弃任务。务必设置 CallerRunsPolicy 或自定义策略记录告警,防止消息静默丢失。当线程池满时,主线程参与发送虽会轻微阻塞,但能保证消息不丢。
2. 消息丢失风险
异步发送后,如果应用重启,内存中的待发送任务会丢失。对于极高重要性的告警(如支付失败),建议结合持久化队列(如 Redis List 或数据库表),确保主业务成功后再消费发送,或采用事务消息方案。
3. 机器人@能力差异
自定义机器人 Webhook 支持 @mobile 或 @all,不支持 @userId。内部应用 Bot 才支持通过 userId 指定用户。配置时需区分场景,避免@无效导致告警被忽略。
4. 频率限制
钉钉机器人有调用频率限制(通常每秒最多 20 条,具体视类型而定)。异步发送虽然不阻塞主线程,但如果瞬间并发过高,依然会触发限流。建议在发送端做本地限流或排队,捕获限流错误码后进行退避重试。
参考文档
- 钉钉开放平台:自定义机器人发送群聊消息
- 钉钉开放平台:加签计算示例
- Spring Framework: ThreadPoolTaskExecutor