接口文檔地址: https://www.coderutil.com/apiopen?tab=hotlist&pk=2000?

兩級(jí)緩存API性能保證

其實(shí)做前幾天的文章中介紹過程序員盒子使用的多集緩存方案,沒有看多的兄弟可以先了解下這篇文章(因?yàn)檫@里我們就不展開講了,直接show me code):

性能優(yōu)化|前端LocalStorage + Google Cache + Redis三級(jí)緩存性能優(yōu)化

其實(shí)服務(wù)端接口這里還是用的Google cache+ redis實(shí)現(xiàn)兩級(jí)緩存,這里并沒有入庫,完全依賴redis:

接口定義

/**
* 微博熱搜
* @return
*/
@GetMapping("/api/resou/v1/weibo")
public APIResponseBean weiboHotSearch(@RequestParam(value = "size", defaultValue = "10") Integer size) {
List<WeiboHotSearchResultBaseVO> result = hotSearchService.getWeiboHotSearchListFromCache(size);
return APIResponseBeanUtil.success(result);
}

針對(duì)跨域請(qǐng)求問題,我們還提供了jsonp跨域接口

/**
* 百度熱搜
* @return
*/
@RequestMapping(value = "/api/resou/v1/weibo.jsonp", produces = "text/script;charset=UTF-8", method= RequestMethod.GET)
public String weiboHotSearchJsonp(HttpServletRequest request, String callback) {
String sizeParam = request.getParameter("size");
int size = StringUtils.isBlank(sizeParam) ? DEFAULT_SIZE : Integer.valueOf(sizeParam);
List<WeiboHotSearchResultBaseVO> result = hotSearchService.getWeiboHotSearchListFromCache(size);
return callback + "(" + JsonUtil.toJsonString(result) + ")";
}

查詢方法

 @Autowired
private RedisService redisService;
@Autowired
private ApiUrlConfig apiUrlConfig;

private static final Cache<String, String> RESOU_LOCAL_CACHE;

static {
RESOU_LOCAL_CACHE = CacheBuilder.newBuilder()
.softValues()
.maximumSize(10L)
.expireAfterWrite(300L, TimeUnit.SECONDS)
.build();
}

/**
* 微博熱搜
* @return
*/
public List<WeiboHotSearchResultBaseVO> getWeiboHotSearchListFromCache(int size) {
String localCacheKey = LocalCacheKey.WEIBO.name();
String localCacheVal = RESOU_LOCAL_CACHE.getIfPresent(localCacheKey);
List<WeiboHotSearchResultBaseVO> list;
if (StringUtils.isNotBlank(localCacheVal)) {
list = JsonUtil.fromJson(localCacheVal, new TypeReference<List<WeiboHotSearchResultBaseVO>>() {});
} else {
String key = RedisKeyEnum.WEIBO_HOT_SEARCH_LIST_CACHE.getKey();
String val = redisService.get(key);
if (StringUtils.isNotBlank(val)) {
list = JsonUtil.fromJson(val, new TypeReference<List<WeiboHotSearchResultBaseVO>>() {});
} else {
// 上篇文章中有這個(gè)刷新方法的具體實(shí)現(xiàn)邏輯
list = refreshAndGetWeiboHotSearchListCache();
}
// 本地緩存如果沒有則刷新到本地緩存
RESOU_LOCAL_CACHE.put(localCacheKey, JsonUtil.toJsonString(list));
}
if (CollectionUtils.isNotEmpty(list)) {
list = list.subList(0, size > list.size() ? list.size() : size);
}
return list;
}

ok,兩集緩存實(shí)現(xiàn)接口響應(yīng)性能到底怎么樣,我們看服務(wù)端日志穩(wěn)定在10ms之內(nèi):

接口AKSK鑒權(quán)

開放z z鑒權(quán)Aksk是做常見的做法之一,這里我們其實(shí)實(shí)現(xiàn)的也比較簡(jiǎn)單,具體方案如下圖所示:

(1)用戶請(qǐng)求接口,header頭邀請(qǐng)攜帶自己的app_key、secret_key

(2)APIFilter攔截API請(qǐng)求,做ak、sk做驗(yàn)證

(3)aksk驗(yàn)證通過請(qǐng)求api,請(qǐng)求緩存獲取數(shù)據(jù)

(4)返回?cái)?shù)據(jù)

APIFilter定義

@Slf4j
@Order(1)
@WebFilter(filterName = "apiRequestFilter", urlPatterns = {"/api/poster/*", "/api/resou/*", "/api/url/*",
"/api/upload/*", "/api/text/*", "/api/ip/*", "/api/music/*", "/api/yulu/*", "/api/openai/*", "/rmi/*"})
public class ApiRequestFilter implements Filter {

@Autowired
private ApiInvokeRecordService apiInvokeRecordService;

@Autowired
private AccessSecretKeyService accessSecretKeyService;

/***
* 開放API清單
*/
private static Map<String, String> openApiMap = new ConcurrentHashMap<>();

private static List<String> WHITE_API_LIST = new ArrayList<>();

static {

// 需要Ak、Sk鑒權(quán)的請(qǐng)求
openApiMap.put("/api/resou/v1/weibo", "微博熱搜");

/**
* 白名單
*/
WHITE_API_LIST.add("/api/poster/qr.temp");

}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
if (isWhiteApiUri(uri)) {
// 跳過鑒權(quán)
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (uri != null && uri.startsWith("rmi")) {
String accessKey = request.getHeader("access-key");
String secretKey = request.getHeader("secret-key");
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
APIResponseBean apiResponse = APIResponseBeanUtil.error(401, "無權(quán)限!");
backErrorMessage(response, apiResponse);
return;
}
}
if (isApiUri(uri)) {
// 獲取API請(qǐng)求的認(rèn)證參數(shù):accessKey secretKey
String accessKey = request.getHeader("access-key");
String secretKey = request.getHeader("secret-key");
// AK、SK 鑒權(quán)(走緩存查詢)
APIResponseBean apiResponseBean = accessSecretKeyService.checkRequestAkSk(accessKey, secretKey);
if (!apiResponseBean.getSuccess()) {
log.error("ApiRequestFilter.checkRequestAkSk error, AK:{}, SK:{}, uri:{}", accessKey, secretKey, uri);
backErrorMessage(response, apiResponseBean);
return;
}
if (StringUtils.isNotBlank(openApiMap.get(uri))) {
// API調(diào)用埋點(diǎn)
apiInvokeRecordService.point(uri, openApiMap.get(uri), accessKey);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}

/***
* 是否需要鑒權(quán)API
* @param uri
* @return
*/
private boolean isApiUri(String uri) {
if (openApiMap.keySet().contains(uri)) {
return true;
}
for (String api : openApiMap.keySet()) {
if (uri.startsWith(api)) {
return true;
}
}
return false;
}

/**
* 是否需要鑒權(quán)API
* @param uri
* @return
*/
private boolean isRmiApiUri(String uri) {
if (openApiMap.keySet().contains(uri)) {
return true;
}
for (String api : openApiMap.keySet()) {
if (uri.startsWith(api)) {
return true;
}
}
return false;
}

/**
* 是否白名單API
* @param uri
* @return
*/
private boolean isWhiteApiUri(String uri) {
return WHITE_API_LIST.contains(uri);
}

private void backErrorMessage(HttpServletResponse response, APIResponseBean apiResponseBean) {
response.setContentType("application/json; charset=UTF-8");
try {
response.getWriter().print(JsonUtil.toJsonString(apiResponseBean));
} catch (IOException e) {
e.printStackTrace();
}
}
}

Filter中我們維護(hù)了需要鑒權(quán)和跳過鑒權(quán)的白名單API、當(dāng)前請(qǐng)求如果需要鑒權(quán),獲取header頭中的ak、sk參數(shù),查詢緩存進(jìn)行aksk認(rèn)證,認(rèn)證通過請(qǐng)求接口。

擴(kuò)展點(diǎn)1

這里的AK、SK是什么時(shí)候生成的、生成規(guī)則又是什么?

答:ak、sk盒子實(shí)在用戶注冊(cè)的時(shí)候就為每個(gè)用戶分配了(這了設(shè)計(jì)的不好,造成aksk的浪費(fèi),因?yàn)榇蟛糠钟脩羝鋵?shí)沒有自己的應(yīng)用、也沒有api調(diào)用的需求,最好是用戶需要的時(shí)候自己出發(fā)生成按鈕,在生成!)

AK、SK在哪里查看?

答:目前在個(gè)人中心和開放api服務(wù)平臺(tái)都可以看到自己的專屬ak、sk:

AKSK生成規(guī)則?

答:跟ID生成器一樣,只要保證唯一就ok,我這里用的簡(jiǎn)單的uuid

既然ak、sk都是唯一的,為啥需要兩個(gè),只生成一個(gè)token不行嗎?

答:也是可以的,能夠做的鑒權(quán)就可以。

為什么業(yè)界一般的做法都有兩個(gè)配對(duì)使用的,有的叫ak、有的叫appId其實(shí)是一樣的,用來標(biāo)識(shí)一個(gè)接入方,而secretKey其實(shí)可以有多個(gè)的,即一個(gè)用戶有多個(gè)sk,方便統(tǒng)計(jì)、計(jì)費(fèi)等,同時(shí)雙重保障API調(diào)用更安全。

擴(kuò)展點(diǎn)2

當(dāng)前分享的方案是一種靜態(tài)sk方案,有同學(xué)在接入云或者其他第三方api的時(shí)候用到了動(dòng)態(tài)sk(token)的方式,看字面意思,靜態(tài)sk指生成一次不會(huì)在變化,動(dòng)態(tài)token及沒次調(diào)用前需要重新請(qǐng)求獲取一次(一般會(huì)有一定的實(shí)效性,避免沒次都要獲取造成的資源浪費(fèi),或者永久有效失去了動(dòng)態(tài)的特點(diǎn))

本文章轉(zhuǎn)載微信公眾號(hào)@coderutil技術(shù)

上一篇:

深入探究MinimalApi是如何在Swagger中展示的

下一篇:

iOS 持續(xù)集成:更完備的 App Store Connect API
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)