企业微信“群机器人”消息合并转发:用Disruptor做环形队列的Java实例

企业微信"群机器人"消息合并转发:用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 的无锁环形队列特性,在高吞吐下实现低延迟消息聚合,有效控制企业微信机器人调用频次,同时保障通知不丢失。环形缓冲区天然具备背压能力,避免突发流量压垮下游接口。

相关推荐
她的男孩11 小时前
从零搭一个企业后台,为什么我把能力拆成 Starter 和 Plugin
java·后端·架构
RainCity11 小时前
Java Swing 自定义组件库分享(七)
java·笔记·后端
Sam_Deep_Thinking11 小时前
连锁门店的外卖订单平台对接
java·微服务·架构·系统架构
_遥远的救世主_12 小时前
从一次结果集密集型查询 OOM 看 Java 服务的稳定性架构治理
java·后端
一楼的猫12 小时前
从工具链视角对比:番茄作家助手 vs 第三方写作辅助方案
java·服务器·开发语言·前端·学习·chatgpt·ai写作
武子康12 小时前
调查研究-138 全球机器人产业深度调研报告【01 篇】:市场规模、竞争格局与商业化成熟 2026
服务器·数据库·ai·chatgpt·机器人·具身智能
likerhood12 小时前
Java static 关键字从浅入深
java·开发语言
_院长大人_13 小时前
Java Excel导出:如何实现自定义表头与字段顺序的完全控制
java·开发语言·后端·excel
磊 子13 小时前
1.4CPU缓存一致性
java·spring cloud·缓存·系统
周末也要写八哥13 小时前
Eclipse 2024全流程网盘下载与安装配置教程详解
java·ide·eclipse