企业微信"群机器人"消息合并转发:用Disruptor做环形队列的Java实例
企业微信群机器人有严格的调用频率限制(如每分钟最多20条消息)。若系统内存在大量低优先级通知(如日志告警、任务完成提醒),直接逐条发送极易触发限流。通过 LMAX Disruptor 构建高性能无锁环形缓冲区,可将短时间内的多条消息聚合为单条 Markdown 消息批量发送,既满足业务通知需求,又规避 API 限频。
消息模型与聚合策略定义
首先定义待转发的消息结构:
java
package wlkankan.cn.wecom.event;
public class WeComMessageEvent {
private String content;
private long timestamp;
private String source;
public WeComMessageEvent(String content, String source) {
this.content = content;
this.source = source;
this.timestamp = System.currentTimeMillis();
}
// getters
public String getContent() { return content; }
public long getTimestamp() { return timestamp; }
public String getSource() { return source; }
}
聚合单元:缓存最多50条或3秒内消息,超限则立即触发发送。
java
package wlkankan.cn.wecom.aggregate;
import wlkankan.cn.wecom.event.WeComMessageEvent;
import java.util.ArrayList;
import java.util.List;
public class MessageBatch {
private final List<WeComMessageEvent> events = new ArrayList<>(50);
private long firstReceivedTime = -1;
public boolean canAdd(WeComMessageEvent event) {
if (events.isEmpty()) {
firstReceivedTime = event.getTimestamp();
}
long now = event.getTimestamp();
return events.size() < 50 && (now - firstReceivedTime) < 3000;
}
public void add(WeComMessageEvent event) {
events.add(event);
}
public boolean isEmpty() {
return events.isEmpty();
}
public List<WeComMessageEvent> getEvents() {
return new ArrayList<>(events);
}
public void clear() {
events.clear();
firstReceivedTime = -1;
}
}

Disruptor 事件与处理器
定义 RingBuffer 中流转的事件:
java
package wlkankan.cn.wecom.disruptor;
import wlkankan.cn.wecom.event.WeComMessageEvent;
public class MessageEvent {
private WeComMessageEvent message;
public void setMessage(WeComMessageEvent message) {
this.message = message;
}
public WeComMessageEvent getMessage() {
return message;
}
}
实现 EventHandler 聚合并发送:
java
package wlkankan.cn.wecom.disruptor;
import wlkankan.cn.wecom.aggregate.MessageBatch;
import wlkankan.cn.wecom.sender.WeComWebhookSender;
import com.lmax.disruptor.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessageAggregationHandler implements EventHandler<MessageEvent> {
private static final Logger log = LoggerFactory.getLogger(MessageAggregationHandler.class);
private final MessageBatch batch = new MessageBatch();
private final WeComWebhookSender sender = new WeComWebhookSender();
private final long flushIntervalNanos = 3_000_000_000L; // 3秒
private long lastFlushTime = System.nanoTime();
@Override
public void onEvent(MessageEvent event, long sequence, boolean endOfBatch) {
if (!batch.canAdd(event.getMessage())) {
flush();
batch.add(event.getMessage());
} else {
batch.add(event.getMessage());
}
// 批次结束或超时则刷新
if (endOfBatch || (System.nanoTime() - lastFlushTime) > flushIntervalNanos) {
flush();
lastFlushTime = System.nanoTime();
}
}
private void flush() {
if (batch.isEmpty()) return;
try {
sender.sendAggregateMessage(batch.getEvents());
} catch (Exception e) {
log.error("Failed to send aggregated message", e);
} finally {
batch.clear();
}
}
}
Webhook 发送器实现
java
package wlkankan.cn.wecom.sender;
import wlkankan.cn.wecom.event.WeComMessageEvent;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
public class WeComWebhookSender {
private static final String WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY";
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public void sendAggregateMessage(List<WeComMessageEvent> events) throws Exception {
String markdown = events.stream()
.map(e -> "- [" + e.getSource() + "] " + e.getContent())
.collect(Collectors.joining("\n"));
String payload = """
{
"msgtype": "markdown",
"markdown": {
"content": "%s"
}
}
""".formatted(markdown.replace("\"", "\\\""));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(WEBHOOK_URL))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("WeCom send failed: " + response.body());
}
}
}
Disruptor 初始化与发布入口
java
package wlkankan.cn.wecom.service;
import wlkankan.cn.wecom.disruptor.MessageEvent;
import wlkankan.cn.wecom.disruptor.MessageAggregationHandler;
import wlkankan.cn.wecom.event.WeComMessageEvent;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.concurrent.Executors;
public class WeComMessageDispatcher {
private final Disruptor<MessageEvent> disruptor;
private final RingBuffer<MessageEvent> ringBuffer;
public WeComMessageDispatcher() {
int bufferSize = 1024; // 必须为2的幂
this.disruptor = new Disruptor<>(
MessageEvent::new,
bufferSize,
Executors.defaultThreadFactory()
);
this.disruptor.handleEventsWith(new MessageAggregationHandler());
this.disruptor.start();
this.ringBuffer = disruptor.getRingBuffer();
}
public void publish(WeComMessageEvent event) {
long sequence = ringBuffer.next();
try {
MessageEvent msgEvent = ringBuffer.get(sequence);
msgEvent.setMessage(event);
} finally {
ringBuffer.publish(sequence);
}
}
public void shutdown() {
disruptor.shutdown();
}
}
使用示例
java
// 在 Spring Bean 中注入
WeComMessageDispatcher dispatcher = new WeComMessageDispatcher();
// 业务代码中调用
dispatcher.publish(new WeComMessageEvent("订单 #12345 已支付", "OrderService"));
dispatcher.publish(new WeComMessageEvent("用户登录异常", "AuthService"));
该方案利用 Disruptor 的无锁环形队列特性,在高吞吐下实现低延迟消息聚合,有效控制企业微信机器人调用频次,同时保障通知不丢失。环形缓冲区天然具备背压能力,避免突发流量压垮下游接口。