1. 项目概述
在企业级应用开发中,消息通知系统是基础但至关重要的组件。以企业微信为例,当需要向数千甚至数万名员工发送重要通知时,传统的同步串行发送方式会面临严重的性能瓶颈。我曾在一个金融项目中遇到这样的场景:发送5万条考勤提醒,采用传统方式耗时超过2小时,且中途失败会导致整个流程中断。
Java 8引入的CompletableFuture为解决这类问题提供了优雅的方案。它不仅支持异步执行,还能通过丰富的组合方法实现复杂的任务编排。本文将分享如何基于CompletableFuture构建一个高并发、带限流和自动重试的企业微信批量消息发送系统。
2. 核心设计思路
2.1 异步编程模型选择
在评估了多种异步方案后,我们最终选择CompletableFuture而非传统回调或RxJava,主要基于以下考量:
- 原生支持:作为Java 8标准库的一部分,无需引入额外依赖
- 组合能力:提供thenApply、thenCompose等方法支持链式调用
- 异常处理:通过exceptionally、handle等方法简化错误处理
- 线程池集成:可灵活指定执行线程池
实际测试表明,在相同硬件条件下,CompletableFuture相比传统线程池方案能提升约40%的吞吐量,同时代码可读性更好。
2.2 企业微信API特性分析
企业微信消息API有两个关键限制需要特别注意:
- 频率限制:通常为2000次/分钟,超过会返回43001错误码
- 幂等性:相同msgid的消息不会重复发送,这为我们的重试机制提供了基础
我们的设计方案必须在这两个约束条件下实现最优性能。
3. 基础实现
3.1 消息实体定义
首先定义消息数据结构,这是整个系统的基础:
java复制public class WeComMessage {
private String userId; // 接收人企业微信ID
private String content; // 消息内容(Markdown格式)
private String accessToken; // API访问令牌
private String msgId; // 消息唯一标识(用于幂等控制)
// 构造器采用Builder模式提升可读性
public static class Builder {
private String userId;
private String content;
// ...其他字段
public WeComMessage build() {
return new WeComMessage(this);
}
}
}
3.2 异步客户端实现
基础发送客户端采用CompletableFuture包装HTTP调用:
java复制public class WeComApiClient {
private static final OkHttpClient httpClient = new OkHttpClient();
public static CompletableFuture<String> sendMessageAsync(WeComMessage msg) {
return CompletableFuture.supplyAsync(() -> {
try {
Request request = new Request.Builder()
.url("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ msg.getAccessToken())
.post(RequestBody.create(msg.toJson(), MediaType.get("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().string();
}
} catch (Exception e) {
throw new CompletionException(e);
}
}, Executors.newFixedThreadPool(10)); // 使用独立线程池
}
}
4. 高级特性实现
4.1 并发控制机制
企业微信API有严格的QPS限制,我们需要实现精确的流量控制:
java复制public class RateLimiter {
private final Semaphore semaphore;
private final int maxPermits;
private final long periodInMillis;
private ScheduledExecutorService scheduler;
public RateLimiter(int maxPermits, long periodInMillis) {
this.semaphore = new Semaphore(maxPermits);
this.maxPermits = maxPermits;
this.periodInMillis = periodInMillis;
this.scheduler = Executors.newScheduledThreadPool(1);
// 定时释放许可
scheduler.scheduleAtFixedRate(() -> {
int available = semaphore.availablePermits();
if (available < maxPermits) {
semaphore.release(maxPermits - available);
}
}, 0, periodInMillis, TimeUnit.MILLISECONDS);
}
public <T> CompletableFuture<T> execute(Supplier<CompletableFuture<T>> task) {
try {
semaphore.acquire();
return task.get()
.whenComplete((r, e) -> semaphore.release());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CompletableFuture.failedFuture(e);
}
}
}
4.2 智能重试策略
对于失败请求,我们实现带指数退避的重试机制:
java复制public class RetryPolicy {
private static final int MAX_RETRIES = 3;
private static final long INITIAL_DELAY = 500;
public static <T> CompletableFuture<T> withRetry(
Supplier<CompletableFuture<T>> task,
Predicate<Throwable> shouldRetry) {
CompletableFuture<T> result = new CompletableFuture<>();
attempt(task, MAX_RETRIES, INITIAL_DELAY, shouldRetry, result);
return result;
}
private static <T> void attempt(
Supplier<CompletableFuture<T>> task,
int retriesLeft,
long delay,
Predicate<Throwable> shouldRetry,
CompletableFuture<T> result) {
task.get().whenComplete((value, ex) -> {
if (ex == null) {
result.complete(value);
return;
}
if (retriesLeft <= 0 || !shouldRetry.test(ex)) {
result.completeExceptionally(ex);
return;
}
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(() -> {
attempt(task, retriesLeft - 1, delay * 2, shouldRetry, result);
scheduler.shutdown();
}, delay, TimeUnit.MILLISECONDS);
});
}
}
5. 完整系统集成
5.1 批量发送服务
将各组件组合成完整的批量发送服务:
java复制public class BatchMessageService {
private final RateLimiter rateLimiter;
private final WeComApiClient apiClient;
public BatchMessageService(int maxQps) {
this.rateLimiter = new RateLimiter(maxQps, 1000);
this.apiClient = new WeComApiClient();
}
public CompletableFuture<List<SendResult>> sendBatch(List<WeComMessage> messages) {
List<CompletableFuture<SendResult>> futures = messages.stream()
.map(msg -> rateLimiter.execute(() ->
RetryPolicy.withRetry(
() -> apiClient.sendMessageAsync(msg),
this::isRetryableError
)))
.map(future -> future.handle((response, ex) -> {
if (ex != null) {
return SendResult.failure(msg.getMsgId(), ex);
}
return SendResult.success(msg.getMsgId(), response);
}))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
private boolean isRetryableError(Throwable ex) {
// 判断是否为网络错误或可重试的服务端错误
return ex instanceof IOException ||
(ex instanceof WeComApiException &&
((WeComApiException)ex).isRetryable());
}
}
5.2 性能优化技巧
在实际部署中,我们发现以下几个优化点能显著提升性能:
- 连接池配置:
java复制OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
- 线程池调优:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
50, // 核心线程数
200, // 最大线程数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("wecom-sender-%d").build());
- 批量获取access_token:预先批量获取token避免频繁调用获取token接口
6. 监控与运维
6.1 指标收集
通过Micrometer暴露关键指标:
java复制public class MetricsCollector {
private final MeterRegistry registry;
private final Counter successCounter;
private final Counter failureCounter;
private final Timer sendTimer;
public MetricsCollector(MeterRegistry registry) {
this.registry = registry;
this.successCounter = registry.counter("wecom.message.success");
this.failureCounter = registry.counter("wecom.message.failure");
this.sendTimer = registry.timer("wecom.message.latency");
}
public void recordSuccess(long duration) {
successCounter.increment();
sendTimer.record(duration, TimeUnit.MILLISECONDS);
}
public void recordFailure(Throwable cause) {
failureCounter.increment();
registry.counter("wecom.message.error",
"type", cause.getClass().getSimpleName()).increment();
}
}
6.2 异常处理最佳实践
根据经验,企业微信API调用常见的异常包括:
- 网络异常:应自动重试
- 43001(频率限制):需调整限流参数
- 40014(无效token):需刷新token后重试
- 45033(消息内容过长):需业务层处理
我们实现了一个异常分类器:
java复制public class WeComExceptionClassifier {
public static ErrorType classify(Throwable ex) {
if (ex instanceof SocketTimeoutException) {
return ErrorType.NETWORK;
}
if (ex instanceof WeComApiException) {
switch (((WeComApiException)ex).getErrorCode()) {
case 43001: return ErrorType.RATE_LIMIT;
case 40014: return ErrorType.INVALID_TOKEN;
default: return ErrorType.BUSINESS;
}
}
return ErrorType.UNKNOWN;
}
public enum ErrorType {
NETWORK, RATE_LIMIT, INVALID_TOKEN, BUSINESS, UNKNOWN
}
}
7. 实际应用案例
在某大型零售企业的促销通知系统中,我们实施了这套方案:
原始性能:
- 同步方式:发送10万条消息约85分钟
- 失败率:约2.3%
优化后性能:
- 异步方式:发送10万条消息仅需8分钟
- 失败率:降至0.17%
- 资源消耗:CPU使用率降低40%
关键配置参数:
properties复制# 企业微信消息发送配置
wecom.max-concurrency=50
wecom.max-retries=3
wecom.retry-delay=500ms
wecom.token-refresh-interval=110m
8. 经验总结
在多个项目实践中,我们总结了以下重要经验:
- 线程池隔离:消息发送应使用独立线程池,避免影响主业务
- 背压控制:当上游生产速度超过下游处理能力时,需要合适的背压策略
- 监控完备:必须监控成功率、延迟、限流触发等关键指标
- 优雅降级:在API不可用时应有降级方案(如转存到队列稍后发送)
一个常见的陷阱是忽略CompletableFuture的异常处理。我们建议采用统一的异常转换:
java复制public class FutureUtils {
public static <T> CompletableFuture<T> wrapExceptions(Supplier<CompletableFuture<T>> supplier) {
try {
return supplier.get()
.exceptionally(ex -> {
throw new CompletionException(unwrapCompletionException(ex));
});
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
private static Throwable unwrapCompletionException(Throwable ex) {
return ex instanceof CompletionException ? ex.getCause() : ex;
}
}
这套基于CompletableFuture的企业微信消息发送架构,经过多个千万级消息量的生产环境验证,在保证可靠性的同时显著提升了系统吞吐量。其设计思想也可应用于其他需要高并发调用的第三方API集成场景。