利用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开发者团队,转载请注明出处!

114

被折叠的 条评论
为什么被折叠?



