做接口需要考慮的問題
什么是接口
接口無非就是客戶端請求你的接口地址,并傳入一堆該接口定義好的參數(shù),通過接口自身的邏輯處理,返回接口約定好的數(shù)據(jù)以及相應的數(shù)據(jù)格式。
接口怎么開發(fā)
接口由于本身的性質(zhì),由于和合作方對接數(shù)據(jù),所以有以下幾點需要在開發(fā)的時候注意:
1.定義接口入?yún)ⅲ簩懞媒涌谖臋n
2.定義接口返回數(shù)據(jù)類型:一般都需要封裝成一定格式,確定返回json還是xml報文等
見如下返回數(shù)據(jù)定義格式:
package com.caiex.vb.model;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Result", propOrder = { "resultCode", "resultMsg" })
public class Result implements Serializable {
private static final long serialVersionUID = 10L;
protected int resultCode;
protected String resultMsg;
public int getResultCode() {
return this.resultCode;
}
public void setResultCode(int value) {
this.resultCode = value;
}
public String getResultMsg() {
return this.resultMsg;
}
public void setResultMsg(String value) {
this.resultMsg = value;
}
}
package com.caiex.vb.model;
import java.io.Serializable;
public class Response implements Serializable {
private static final long serialVersionUID = 2360867989280235575L;
private Result result;
private Object data;
public Result getResult() {
if (this.result == null) {
this.result = new Result();
}
return result;
}
public void setResult(Result result) {
this.result = result;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
3.確定訪問接口的方式,get or post等等,可以根據(jù)restful接口定義規(guī)則RESTful API:RESTful API
4.定義一套全局統(tǒng)一并通用的返回碼,以幫助排查問題;
public static int NO_AGENT_RATE = 1119; //未找到兌換率
public static int SCHEME_COMMIT_FAIL = 4000; //方案提交失敗
public static int SCHEME_CONFIRMATION = 4001; //方案確認中
public static int SCHEME_NOT_EXIST = 4002; //方案不存在
public static int SCHEME_CANCEL= 4005; //方案不存在
//。。。。
5.統(tǒng)一的異常處理:應該每個系統(tǒng)都需要一套統(tǒng)一的異常處理
package com.caiex.vb.interceptor;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.caiex.vb.model.Response;
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 所有異常報錯
* @param request
* @param exception
* @return
* @throws Exception
*/
@ExceptionHandler(value=Exception.class)
public Response allExceptionHandler(HttpServletRequest request,
Exception exception) throws Exception
{
logger.error("攔截到異常:", exception);
Response response = new Response();
response.setData(null);
response.getResult().setResultCode(9999);
response.getResult().setResultMsg("系統(tǒng)繁忙");
return response;
}
}
6.攔截器鏈設置:合作方訪問接口的時候,會根據(jù)你接口定義好的傳參訪問你的接口服務器,但是會存在接口參數(shù)類型錯誤或者格式不對,必傳參數(shù)沒傳的問題,甚至一些惡意請求,都可以通過攔截器鏈進行前期攔截,避免造成接口服務的壓力。
學習資料:Java進階視頻資源
還有很重要的一點,加簽驗簽也可以在攔截器設置。繼承WebMvcConfigurerAdapter實現(xiàn)springboot的攔截器鏈。實現(xiàn)HandlerInterceptor方法編寫業(yè)務攔截器。
package com.caiex.vb.interceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import com.caiex.redis.service.api.RedisApi;
import com.caiex.vb.model.Response;
import com.caiex.vb.utils.CaiexCheckUtils;
@Component
public class SignInterceptor extends BaseValidator implements HandlerInterceptor{
private Logger logger = LogManager.getLogger(this.getClass());
@Resource
private RedisApi redisApi;
public void afterCompletion(HttpServletRequest arg0,
HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
// TODO Auto-generated method stub
}
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,
Object arg2, ModelAndView arg3) throws Exception {
// TODO Auto-generated method stub
}
public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1,
Object arg2) throws Exception {
if(isTestIpAddr(arg0)){
return true;
}
String securityKey = redisApi.hGet("securityKey", arg0.getParameter("agentid"));
if(StringUtils.isEmpty(securityKey)){
Response response = new Response();
response.setData(null);
response.getResult().setResultCode(8001);
response.getResult().setResultMsg("缺少私鑰, 渠道號:" + arg0.getParameter("agentid"));
logger.error("缺少私鑰, 渠道號:" + arg0.getParameter("agentid"));
InterceptorResp.printJson(arg1, response);
return false;
}
if(StringUtils.isEmpty(arg0.getParameter("sign")) || !arg0.getParameter("sign").equals(CaiexCheckUtils.getSign(arg0.getParameterMap(), securityKey))){
Response response = new Response();
response.setData(null);
response.getResult().setResultCode(3203);
response.getResult().setResultMsg("參數(shù)簽名認證失敗");
logger.error("參數(shù)簽名認證失敗:" + JSON.toJSONString(arg0.getParameterMap()) + " securityKey = " + securityKey);
InterceptorResp.printJson(arg1, response);
return false;
}else{
return true;
}
}
}
package com.caiex.oltp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.caiex.oltp.interceptor.APILimitRateValidator;
import com.caiex.oltp.interceptor.CommonValidator;
import com.caiex.oltp.interceptor.DDSAuthValidator;
import com.caiex.oltp.interceptor.QueryPriceParamsValidator;
import com.caiex.oltp.interceptor.TradeParamsValidator;
@EnableWebMvc
@Configuration
@ComponentScan
public class WebAppConfigurer extends WebMvcConfigurerAdapter {
@Bean
CommonValidator commonInterceptor() {
return new CommonValidator();
}
@Bean
DDSAuthValidator ddsAuthInterceptor() {
return new DDSAuthValidator();
}
@Bean
QueryPriceParamsValidator queryPriceParamsInterceptor() {
return new QueryPriceParamsValidator();
}
@Bean
TradeParamsValidator tradeParamsInterceptor() {
return new TradeParamsValidator();
}
@Bean
APILimitRateValidator aPILimitRateInterceptor() {
return new APILimitRateValidator();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//訪問速率限制
registry.addInterceptor(aPILimitRateInterceptor())
.addPathPatterns("/*/*");
//.addPathPatterns("/price/getPriceParam");
//參數(shù)簽名認證
registry.addInterceptor(ddsAuthInterceptor())
.addPathPatterns("/tradeState/*")
.addPathPatterns("/recycle/*")
.addPathPatterns("/matchInfo/*")
.addPathPatterns("/price/tradeTicketParam");
//公共參數(shù)檢查
registry.addInterceptor(commonInterceptor())
.addPathPatterns("/price/tradeTicketParam")
.addPathPatterns("/tradeState/*")
.addPathPatterns("/recycle/*");
//詢價參數(shù)校驗
registry.addInterceptor(queryPriceParamsInterceptor())
.addPathPatterns("/price/getPriceParam");
//交易參數(shù)檢查
registry.addInterceptor(tradeParamsInterceptor())
.addPathPatterns("/price/tradeTicketParam");
super.addInterceptors(registry);
}
}
7.token令牌和sign數(shù)字簽名實現(xiàn)數(shù)據(jù)保密性。
創(chuàng)建令牌(Token)
為保證請求的合法性,我們提供第三方創(chuàng)建令牌接口,某些接口需要通過token驗證消息的合法性,以免遭受非法攻擊。
token過期時間目前暫時定為1天,由于考慮到合作方往往是分布式環(huán)境,多臺機器都有可能申請token,為了降低合作方保證token一致性的難度,調(diào)用接口創(chuàng)建token成功以后一分鐘以內(nèi),再次請求token返回的數(shù)據(jù)是一樣的。
獲取私鑰
獲取用于數(shù)字簽名的私鑰,第三方獲取的私鑰需妥善保存,并定期更新,私鑰只參與數(shù)字簽名,不作為參數(shù)傳輸。
數(shù)字簽名方式:
參數(shù)簽名;簽名方式:所有值不為null的參數(shù)(不包括本參數(shù))均參與數(shù)字簽名,按照“參數(shù)名+參數(shù)值+私鑰”的格式得到一個字符串,再將這個字符串MD5一次就是這個參數(shù)的值。(示例:h15adc39y9ba59abbe56e057e60f883g
),所以需要先獲取私鑰。
驗簽方式:
將用戶的所有非null參數(shù)放入定義好排序規(guī)則的TreeSet中進行排序,再用StringBuilder按照按照“參數(shù)名+參數(shù)值+私鑰”的格式得到一個字符串(私鑰從redis拿),再將這個字符串MD5一次就是這個參數(shù)的值。將這個值與用戶傳來的sign簽名對比,相同則通過,否則不通過。
private String createToken(){
String utk = "Msk!D*"+System.currentTimeMillis()+"UBR&FLP";
logger.info("create token --- "+Md5Util.md5(utk));
return Md5Util.md5(utk);
}
8.接口限流
有時候服務器壓力真的太大,以防交易接口被擠死,就可以對一些其他不影響主要業(yè)務功能并且計算量大的接口做限流處理。RateLimit--使用guava來做接口限流,當接口超過指定的流量時,就不處理該接口的請求。詳細可看RateLimit。也可參考其他限流框架。
9.協(xié)議加密,http升級成https;
為什么要升級呢,為了保證數(shù)據(jù)的安全性。當使用https訪問時,數(shù)據(jù)從客戶端到服務斷,服務端到客戶端都加密,即使黑客抓包也看不到傳輸內(nèi)容。當然還有其他好處,這里不多講。但這也是開發(fā)接口項目需要注意的一個問題。
如何提高接口的高并發(fā)和高可用
接口開發(fā)好了,接下來就討論接口的可用性問題。首先我們要將高并發(fā)和高可用區(qū)分一下,畢竟高可用是在可用的情況,只是很慢或者效率不高。其實也可以歸為一類問題,但是不重要啦,重要的是怎么提高你寫的接口的訪問速度和性能。
接口的高并發(fā)解決方案(其實沒有唯一答案,業(yè)界針對不同業(yè)務也有很多不同的方法)
當訪問一個接口獲取數(shù)據(jù)時,發(fā)現(xiàn)返回很慢,或者總是超時,如果排除網(wǎng)絡的原因,那就是接口服務器壓力太大,處理不過來了。在世界杯期間,我們查看后臺日志總是connection by reset和borker pipe和一些超時問題。
這時候,你可能遇到了高并發(fā)和高可用問題。但是,不管遇到什么問題,都不能臆斷和亂改,你得需要找到慢的原因,才能對癥下藥,亂改可能會導致其他問題的出現(xiàn)。首先,解決高并發(fā)問題的三個方向是負載均衡,緩存和集群。
學習資料:Java進階視頻資源
負載均衡
我們使用的是阿里云服務器的負載均衡,后臺分布式服務管理,我們運維小哥哥搭建了一套k8s,可以自由在k8s上擴展服務節(jié)點,各個服務結(jié)點也能隨內(nèi)存的使用自動漂移,不用多說,k8s真的很厲害,感興趣的同學可以詳細去學。那么問題來了,阿里云的負載均衡怎么對應到k8s的負載均衡呢?
這個涉及到了k8s的service暴露的一些特點,簡單說就是k8s把所有集群的服務都通過指定的內(nèi)部負載均衡,在指定的服務器上暴露,然后我們又把這幾個服務器接在阿里云負載均衡下,這個涉及的細節(jié)和配置很多。當然,除nginx外,還有其他負載均衡解決方案,軟件硬件都有,硬件如f5等。
阿里云的nginx負載均衡,我們使用的是加權(quán)輪詢策略,其實輪詢是最低效的方式;
這就是最基本的負載均衡實例,但這不足以滿足實際需求;目前Nginx服務器的upstream模塊支持6種方式的分配:
負載均衡策略
輪詢 | 默認方式 |
---|
weight | 權(quán)重方式 |
ip_hash | 依據(jù)ip分配方式 |
least_conn | 最少連接方式 |
fair(第三方) | 響應時間方式 |
url_hash(第三方) | 依據(jù)URL分配方式 |
集群
首先,通過排查問題,發(fā)現(xiàn)是oltpapi接口服務處理請求很慢,大量請求過來,總是超時和中斷連接,這時候,我們想著最簡單的方法就是加機器,給oltp接口服務多加幾臺機器。
嗯,一切都很完美,如預期進行,但是加到一定數(shù)量,你發(fā)現(xiàn),怎么不起效果,異步響應還是很慢,或者更直觀的說,消息隊列出現(xiàn)了嚴重的消息堆積。這時候,你發(fā)現(xiàn)出現(xiàn)了新的問題或者瓶頸,這個問題已經(jīng)不是說加oltp服務器能解決了,那么,就需要去重新定位問題。發(fā)現(xiàn)是消息堆積,消息堆積就是生產(chǎn)者過快,導致消費者消費不過來,這時候,你就需要增加消費者的消費數(shù)量。給風控系統(tǒng)多加幾臺機器,讓消費者和生產(chǎn)者達到一定平衡。
這里有個誤區(qū),你可能以為是rocketmq的broker數(shù)量過少,增加broker數(shù)量,其實當消費者和生產(chǎn)者保持一樣的速度時,消息肯定不對堆積,按照原始的broker數(shù)量就足夠。但是增加broker也會使得消息得到盡快的處理,提升一定效率。
緩存
當加機器不能解決問題時,或者說沒那么多服務器可使用時,那么就要重代碼層面解決高并發(fā)問題。Redis 是一個高性能的key-value數(shù)據(jù)庫,當獲取數(shù)據(jù)從數(shù)據(jù)庫拿很慢時,就可以存儲到redis,從redis取值。
- 用ConcurrentHashMap緩存對象,并設置過期時間
- redis緩存數(shù)據(jù),結(jié)合spring定時任務定時獲取不會經(jīng)常改動的key
- 提高使用redis的效率:比如使用mGet一次獲取多個key
接口高可用問題
高可用問題應該上升到整個服務的架構(gòu)問題上,就是說在搭建整體系統(tǒng)是就應該考慮到。高可用問題是以單點故障,訪問速度慢的問題為主導。見 服務高可用
- redis主從分布式(redis的單點故障和訪問速度的提高和主從備份)
總結(jié)
下面對接口開發(fā)服務做一些總結(jié):
1.是拉還是推:
當接口作為數(shù)據(jù)源時,還要考慮數(shù)據(jù)是讓合作方主動過來拉還是數(shù)據(jù)有變化就推送呢,當然是推的效果更好,但是如何有效的推數(shù)據(jù),不推重復數(shù)據(jù)等都是需要根據(jù)實際業(yè)務考慮的問題。
2.多臺分布式服務器上,怎么保證交易的冪等和訂單的唯一性
當接口服務和合作方都處于分布式情況下,就很容易出現(xiàn)一個訂單號申請多次交易請求,但是根據(jù)冪等性,一張彩票只能交易一次,并且每次不管何時請求,結(jié)果都應該一樣不會改變。這種情況下,我們怎么保證唯一性呢,我們需要把該訂單和訂單狀態(tài)存redis,每次請求時去看是否訂單已存在。但可能這次交易不成功,下次這張票還可以繼續(xù)交易,可以生成新的訂單號啊。
redis的setNX是一個很好的解決方案,意思是當存在該key時,返回false,當沒有時,該key和value插入成功。用作檢查訂單是否正在提交,如果是,則阻塞本次請求,避免重復提交 ,可以設置過期時間3s。提交之前鎖定訂單,防止重復提交。
3.處理時間超過10s,自動返回該訂單交易失敗
總之,在高并發(fā)場景下,導致服務崩潰的原因還是redis和數(shù)據(jù)庫,可能是redis讀寫太慢,或者數(shù)據(jù)庫的一些sql使用不當,或者沒建索引導致讀寫很慢。
該文章在 2024/1/13 16:40:19 編輯過