利用Caffeine + Redis二级缓存减少重复调用外卖霸王餐api接口的频次

利用Caffeine + Redis二级缓存减少重复调用外卖霸王餐api接口的频次

在“吃喝不愁”App对接美团、饿了么等平台的霸王餐API时,频繁请求相同参数(如活动ID、门店ID)会导致配额超限或响应延迟。为降低对外部API的依赖,本文采用 Caffeine(本地缓存)+ Redis(分布式缓存)的二级缓存架构,在保证数据一致性的同时显著提升系统吞吐量与容错能力。

1. 依赖引入

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2. 定义缓存Key与Value模型

package baodanbao.com.cn.cache.model;

public class FreeMealActivityCacheKey {
    private final String platform; // meituan / eleme
    private final String activityId;

    public FreeMealActivityCacheKey(String platform, String activityId) {
        this.platform = platform;
        this.activityId = activityId;
    }

    @Override
    public boolean equals(Object o) { /* 省略 */ }
    @Override
    public int hashCode() { /* 省略 */ }
    @Override
    public String toString() {
        return platform + ":" + activityId;
    }
}

缓存值为 API 响应体:

package baodanbao.com.cn.cache.model;

import java.io.Serializable;

public class FreeMealActivityInfo implements Serializable {
    private String name;
    private String status;
    private Integer quota;
    private Long startTime;
    private Long endTime;
    // getters/setters
}

在这里插入图片描述

3. 二级缓存管理器实现

package baodanbao.com.cn.cache.service;

import baodanbao.com.cn.cache.model.FreeMealActivityCacheKey;
import baodanbao.com.cn.cache.model.FreeMealActivityInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Service
public class FreeMealActivityCacheService {

    private final Cache<String, FreeMealActivityInfo> localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(2, TimeUnit.MINUTES)
            .build();

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    private static final String REDIS_PREFIX = "free_meal:activity:";

    public FreeMealActivityInfo get(FreeMealActivityCacheKey key, Supplier<FreeMealActivityInfo> loader) {
        String cacheKey = key.toString();

        // 1. 查本地缓存
        FreeMealActivityInfo local = localCache.getIfPresent(cacheKey);
        if (local != null) {
            return local;
        }

        // 2. 查Redis
        String redisValue = redisTemplate.opsForValue().get(REDIS_PREFIX + cacheKey);
        if (redisValue != null) {
            try {
                FreeMealActivityInfo remote = objectMapper.readValue(redisValue, FreeMealActivityInfo.class);
                localCache.put(cacheKey, remote); // 回填本地
                return remote;
            } catch (JsonProcessingException e) {
                // 忽略,降级到loader
            }
        }

        // 3. 调用源(如美团API)
        FreeMealActivityInfo fresh = loader.get();
        if (fresh != null) {
            // 写入Redis(TTL 10分钟)
            try {
                String json = objectMapper.writeValueAsString(fresh);
                redisTemplate.opsForValue().set(REDIS_PREFIX + cacheKey, json, 10, TimeUnit.MINUTES);
            } catch (JsonProcessingException ignored) {}
            // 写入本地
            localCache.put(cacheKey, fresh);
        }
        return fresh;
    }

    public void evict(FreeMealActivityCacheKey key) {
        String k = key.toString();
        localCache.invalidate(k);
        redisTemplate.delete(REDIS_PREFIX + k);
    }
}

4. 在API调用层集成缓存

package baodanbao.com.cn.meituan.client;

import baodanbao.com.cn.cache.model.FreeMealActivityCacheKey;
import baodanbao.com.cn.cache.model.FreeMealActivityInfo;
import baodanbao.com.cn.cache.service.FreeMealActivityCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CachedMeituanApiClient {

    @Autowired
    private MeituanRawApiClient rawClient; // 实际调用HTTP的客户端

    @Autowired
    private FreeMealActivityCacheService cacheService;

    public FreeMealActivityInfo getActivity(String activityId) {
        FreeMealActivityCacheKey key = new FreeMealActivityCacheKey("meituan", activityId);
        return cacheService.get(key, () -> rawClient.fetchActivityFromMeituan(activityId));
    }
}

5. 缓存更新策略

当运营后台修改活动状态时,主动失效缓存:

package baodanbao.com.cn.bajie.service;

import baodanbao.com.cn.cache.model.FreeMealActivityCacheKey;
import baodanbao.com.cn.cache.service.FreeMealActivityCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ActivityAdminService {

    @Autowired
    private FreeMealActivityCacheService cacheService;

    public void updateActivityStatus(String platform, String activityId) {
        // 更新DB逻辑...
        // 失效缓存
        cacheService.evict(new FreeMealActivityCacheKey(platform, activityId));
    }
}

6. 防止缓存穿透与雪崩

  • 空值缓存:若 loader.get() 返回 null,可缓存一个 NULL_PLACEHOLDER 对象(带短TTL),避免反复击穿。
  • 随机TTL:Redis写入时使用 9~11分钟 随机过期时间,分散失效压力。
  • 熔断降级:在 loader 中加入 Hystrix 或 Resilience4j,防止API异常导致线程阻塞。

示例:空值处理(简化版)

if (fresh == null) {
    FreeMealActivityInfo placeholder = new FreeMealActivityInfo(); // 标记为空
    redisTemplate.opsForValue().set(REDIS_PREFIX + cacheKey, "{}", 1, TimeUnit.MINUTES);
    localCache.put(cacheKey, placeholder);
    return null;
}

7. 监控指标埋点

通过 Micrometer 暴露缓存命中率:

MeterRegistry registry = ...;
Counter localHit = Counter.builder("cache.hit").tag("layer", "local").register(registry);
Counter redisHit = Counter.builder("cache.hit").tag("layer", "redis").register(registry);
Counter miss = Counter.builder("cache.miss").register(registry);
// 在 get 方法中对应位置 increment

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值