高并发场景下查券返利机器人的请求合并与缓存预热策略(Redis + Caffeine 实践)

高并发场景下查券返利机器人的请求合并与缓存预热策略(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 研发团队,转载请注明出处!

相关推荐
tudficdew2 小时前
使用Flask快速搭建轻量级Web应用
jvm·数据库·python
茁壮成长的露露2 小时前
PMM监控MongoDB
数据库·mongodb
Funky_oaNiu2 小时前
Oracle如何将用户下的一个表空间的数据迁移到另一个表空间
数据库·oracle
爱学习的阿磊2 小时前
使用XGBoost赢得Kaggle比赛
jvm·数据库·python
Full Stack Developme2 小时前
数据存储的底层都是字符,但在使用时候怎么能变化出各种字段类型
数据库
什么都不会的Tristan2 小时前
MySQL篇
数据库·mysql
Geoking.2 小时前
Redis 的 RDB 与 AOF:持久化机制全解析
数据库·redis·缓存
鱼跃鹰飞3 小时前
面试题:说一说redis和Memcached的区别
数据库·redis·memcached
深念Y3 小时前
中兴微随身WiFi 板号UZ901_v1.6 影腾Y1新版本 增加SIM卡槽 开启ADB 去云控 改串号教程 下
数据库·adb