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

相关推荐
颇有几分姿色2 小时前
Spring Boot Actuator未授权访问漏洞 【原理扫描】修复
java·spring boot·后端
1candobetter2 小时前
JAVA后端开发——Spring Boot 多环境配置与实践
java·开发语言·spring boot
沛沛老爹2 小时前
Web开发者实战:多模态Agent技能开发——语音交互与合成技能集成指南
java·开发语言·前端·人工智能·交互·skills
Wpa.wk2 小时前
Docke-compose 搭建 testLink环境
java·经验分享·测试工具·容器·testlink
小北方城市网2 小时前
Spring Boot Actuator+Prometheus+Grafana 生产级监控体系搭建
java·spring boot·python·rabbitmq·java-rabbitmq·grafana·prometheus
shehuiyuelaiyuehao2 小时前
图书管理系统
java·服务器·前端
IUGEI4 小时前
从原理到落地:DAG在大数据SLA中的应用
java·大数据·数据结构·后端·算法
Whisper_Sy10 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 网络状态实现
android·java·开发语言·javascript·网络·flutter·php
乂爻yiyao10 小时前
1.1 JVM 内存区域划分
java·jvm