高并发场景下查券返利机器人的请求合并与缓存预热策略(Redis + Caffeine 实践)
大家好,我是 微赚淘客系统3.0 的研发者省赚客!
在微赚淘客系统3.0中,查券返利机器人是核心功能之一。用户通过输入商品链接,系统自动查询优惠券并返回返利信息。随着用户量激增,单日请求峰值突破百万级,对后端服务的吞吐能力提出了严峻挑战。为应对高并发场景,我们设计了一套基于请求合并(Request Batching)与多级缓存(Redis + Caffeine)的优化方案。
一、问题背景:高频重复请求导致资源浪费
在实际运行中,大量用户会同时查询同一商品的优惠券信息,造成对第三方接口的重复调用,不仅浪费带宽,还容易触发限流。例如,某爆款商品在直播期间被数千人同时查询,若不加处理,将产生数千次相同的外部API请求。
二、请求合并机制设计
为减少冗余请求,我们引入了请求合并机制。其核心思想是:将短时间内相同参数的请求聚合为一个批量请求,统一调用下游服务,再将结果分发给各个原始请求。
我们使用 CompletableFuture 实现异步合并逻辑,并借助 Guava 的 RateLimiter 控制批处理频率。
java
package juwatech.cn.coupon.service;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class CouponBatcher {
private final ConcurrentHashMap<String, CompletableFuture<String>> pendingRequests = new ConcurrentHashMap<>();
private final RateLimiter batchLimiter = RateLimiter.create(50); // 每秒最多50批
public CompletableFuture<String> queryCouponAsync(String itemId) {
return pendingRequests.computeIfAbsent(itemId, key -> {
CompletableFuture<String> future = new CompletableFuture<>();
scheduleBatchIfNeeded();
return future;
});
}
private void scheduleBatchIfNeeded() {
if (batchLimiter.tryAcquire()) {
processBatch();
}
}
private void processBatch() {
var batch = new ConcurrentHashMap<>(pendingRequests);
pendingRequests.clear();
CompletableFuture.runAsync(() -> {
try {
// 调用下游服务,假设返回 Map<itemId, couponInfo>
var results = ExternalCouponService.batchQuery(batch.keySet());
batch.forEach((itemId, future) -> {
String result = results.getOrDefault(itemId, "");
future.complete(result);
});
} catch (Exception e) {
batch.values().forEach(f -> f.completeExceptionally(e));
}
});
}
}
三、多级缓存架构:Caffeine + Redis
为降低对合并层的压力,我们在请求入口处加入多级缓存。优先读取本地缓存(Caffeine),未命中则穿透至分布式缓存(Redis),仍未命中才进入请求合并流程。
1. 本地缓存(Caffeine)配置
java
package juwatech.cn.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class LocalCouponCache {
private static final Cache<String, String> CACHE = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
public static String get(String key) {
return CACHE.getIfPresent(key);
}
public static void put(String key, String value) {
CACHE.put(key, value);
}
}
2. Redis 缓存操作封装
java
package juwatech.cn.cache;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisCouponCache {
private final StringRedis template;
public RedisCouponCache(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String get(String key) {
return redisTemplate.opsForValue().get("coupon:" + key);
}
public void set(String key, String value, long ttlSeconds) {
redisTemplate.opsForValue().set("coupon:" + key, value, ttlSeconds, TimeUnit.SECONDS);
}
}
3. 缓存读取与回源逻辑
java
package juwatech.cn.coupon.handler;
import juwatech.cn.cache.LocalCouponCache;
import juwatech.cn.cache.RedisCouponCache;
import juwatech.cn.coupon.service.CouponBatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class CouponQueryHandler {
@Autowired
private RedisCouponCache redisCache;
@Autowired
private CouponBatcher batcher;
public CompletableFuture<String> handle(String itemId) {
// 1. 本地缓存
String local = LocalCouponCache.get(itemId);
if (local != null) {
return CompletableFuture.completedFuture(local);
}
// 2. Redis 缓存
String remote = redisCache.get(itemId);
if (remote != null) {
LocalCouponCache.put(itemId, remote);
return CompletableFuture.completedFuture(remote);
}
// 3. 请求合并
return batcher.queryCouponAsync(itemId)
.thenApply(result -> {
if (result != null && !result.isEmpty()) {
LocalCouponCache.put(itemId, result);
redisCache.set(itemId, result, 120); // TTL 2分钟
}
return result;
});
}
}
四、缓存预热策略
针对热点商品,我们采用定时任务进行缓存预热。通过分析历史数据,识别出未来可能爆火的商品ID列表,提前加载至 Redis 和 Caffeine。
java
package juwatech.cn.task;
import juwatech.cn.cache.RedisCouponCache;
import juwatech.cn.coupon.service.ExternalCouponService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class CouponPreheatTask {
private final RedisCouponCache redisCache;
public CouponPreheatTask(RedisCouponCache redisCache) {
this.redisCache = redisCache;
}
@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
public void preheatHotItems() {
List<String> hotItemIds = HotItemAnalyzer.getTop100(); // 获取预测热点
var results = ExternalCouponService.batchQuery(hotItemIds);
results.forEach((itemId, coupon) -> {
redisCache.set(itemId, coupon, 300); // 预热缓存5分钟
});
}
}
通过上述组合策略,系统在压测中 QPS 提升近 8 倍,第三方接口调用量下降 92%,有效支撑了大促期间的流量洪峰。
本文著作权归 微赚淘客系统3.0 研发团队,转载请注明出处!